Custom UI & Theming
In this course, you will learn how to customize the look and feel of your desktop app. Frameless windows, custom title bars, sidebar navigation, dark theme CSS variables, shadcn/ui components, and resizable panels.
The Frameless Window
Grit desktop apps use frameless windows by default. The setting lives in wails.json:
{
"name": "myapp",
"frontend:dir": "./frontend",
"frontend:install": "pnpm install",
"frontend:build": "pnpm build",
"frontend:dev:watcher": "pnpm dev",
"frontend:dev:serverUrl": "auto",
"author": {
"name": ""
},
"info": {},
"nsisType": "exe",
"obfuscated": false,
"garbled": false,
"frameless": true,
"width": 1024,
"height": 768,
"minWidth": 800,
"minHeight": 600
}When frameless is true, the OS title bar disappears entirely. Your React app fills the entire window. This means you need to build your own title bar with window controls.
Challenge: Find the Frameless Setting
Open wails.json in your desktop project. Find the frameless setting. What are the width and height values? Try changing frameless to false, restart the app, and see what happens — you'll get the default OS title bar.
Custom Title Bar
Since there's no OS title bar, Grit scaffolds a custom one with React. It includes the app name and window control buttons (minimize, maximize, close). These buttons call Wails runtime functions:
import { WindowMinimise, WindowToggleMaximise, WindowClose } from '../../wailsjs/runtime'
export function TitleBar() {
return (
<div className="flex items-center justify-between h-10 bg-background border-b border-border px-4"
style={{ "--wails-draggable": "drag" } as React.CSSProperties}>
<span className="text-sm font-medium text-foreground">My App</span>
<div className="flex items-center gap-1" style={{ "--wails-draggable": "no-drag" } as React.CSSProperties}>
<button onClick={() => WindowMinimise()} className="p-1.5 hover:bg-bg-hover rounded">
{/* Minimize icon */}
</button>
<button onClick={() => WindowToggleMaximise()} className="p-1.5 hover:bg-bg-hover rounded">
{/* Maximize icon */}
</button>
<button onClick={() => WindowClose()} className="p-1.5 hover:bg-red-500/20 text-red-400 rounded">
{/* Close icon */}
</button>
</div>
</div>
)
}The Wails runtime provides these functions:
- •
WindowMinimise()— minimize the window to the taskbar - •
WindowToggleMaximise()— toggle between maximized and normal size - •
WindowClose()— close the application - •
WindowSetTitle(title)— change the window title programmatically
Challenge: Explore the Title Bar
Find the title bar component in your project (look in frontend/src/components/). What Wails runtime functions does it import? Click each window control button — do minimize, maximize, and close all work?
Draggable Window
Without the OS title bar, users cannot drag the window to move it. Wails solves this with a CSS property: --wails-draggable. Any element with this property set to drag becomes a drag handle:
{/* This div can be dragged to move the window */}
<div style={{ "--wails-draggable": "drag" } as React.CSSProperties}>
<span>Drag me to move the window</span>
{/* Buttons inside should NOT be draggable */}
<div style={{ "--wails-draggable": "no-drag" } as React.CSSProperties}>
<button>Click me (not draggable)</button>
</div>
</div>The title bar uses --wails-draggable: drag so you can grab it and move the window. The window control buttons use --wails-draggable: no-drag so clicking them triggers the button action instead of starting a drag.
Challenge: Test Dragging
Try dragging the window by the title bar area — it should move. Now try dragging by the main content area — it should not move. Can you identify the exact CSS property that makes this work?
Sidebar Navigation
The sidebar is auto-generated from your resources. When you run grit generate resource, a new sidebar entry is injected automatically with an appropriate icon:
const sidebarItems = [
{ label: "Dashboard", icon: LayoutDashboard, href: "/" },
{ label: "Blogs", icon: FileText, href: "/blogs" },
{ label: "Contacts", icon: Users, href: "/contacts" },
// Automatically added when you generate resources:
{ label: "Tasks", icon: CheckSquare, href: "/tasks" },
{ label: "Notes", icon: StickyNote, href: "/notes" },
]The sidebar is collapsible — it can shrink to just icons, giving more space to the main content. Grit picks Lucide icons based on the resource name (e.g., "Task" gets a check icon,"Note" gets a sticky note icon).
Challenge: Sidebar Auto-Updates
Generate 3 new resources (e.g., Project, Invoice, Report). Restart the app. Does the sidebar show 3 new entries with icons? Check the sidebar component to see how the entries are defined.
Dark Theme
Grit desktop apps use a premium dark theme defined with CSS custom properties (variables). These variables are used by all shadcn/ui components and your custom components:
:root {
--background: #0a0a0f; /* Darkest — main background */
--bg-elevated: #22222e; /* Card surfaces */
--bg-hover: #2a2a38; /* Hover states */
--border: #2a2a3a; /* Borders and dividers */
--foreground: #e8e8f0; /* Primary text */
--text-secondary: #9090a8; /* Secondary text */
--text-muted: #606078; /* Muted/disabled text */
--primary: #6c5ce7; /* Accent purple */
--primary-hover: #7c6cf7; /* Accent hover */
--success: #00b894; /* Green for success states */
--danger: #ff6b6b; /* Red for errors and delete */
--warning: #fdcb6e; /* Yellow for warnings */
}You use these variables through Tailwind CSS classes:
- •
bg-background— the darkest background (#0a0a0f) - •
bg-bg-elevated— card and panel surfaces (#22222e) - •
text-foreground— primary white text (#e8e8f0) - •
text-primary— accent purple (#6c5ce7) - •
border-border— subtle borders (#2a2a3a)
--primary from purple to blue and every button, link, and accent in the entire app updates instantly. No need to find and replace colors across dozens of files.Challenge: Customize the Theme
Find the CSS file with theme variables (look in frontend/src/). Change the --primary color from #6c5ce7 to #3b82f6 (blue). Save and check — does the entire app's accent color update?
shadcn/ui in Desktop
Grit desktop apps use the same shadcn/ui components as web projects. These are unstyled, accessible React components that you can customize:
| Component | Used for |
|---|---|
| Button | Primary actions, form submissions, navigation |
| Input | Text fields in forms |
| Dialog | Modal dialogs for confirmations and forms |
| DataTable | Sortable, filterable data tables for listing resources |
| Select | Dropdown selection (e.g., category picker) |
| Checkbox | Boolean toggles (e.g., task done/not done) |
| Textarea | Multi-line text input for descriptions |
All components live in frontend/src/components/ui/ and are imported like:
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog'Challenge: Find shadcn/ui Components
Search your project for imports from @/components/ui/. Find at least 5 different shadcn/ui components used in the app. Which component is used most frequently?
Resizable Panels
Grit desktop apps can include resizable split panels, similar to IDE-style interfaces where you can drag a divider between the sidebar and main content to adjust their widths:
import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from '@/components/ui/resizable'
export function AppLayout({ children }) {
return (
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={20} minSize={15} maxSize={30}>
<Sidebar />
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={80}>
{children}
</ResizablePanel>
</ResizablePanelGroup>
)
}The ResizableHandle is the draggable divider between panels. Users can grab it and drag to resize. The minSize and maxSize props prevent panels from becoming too small or too large.
Challenge: Test Resizable Panels
If your app has resizable panels, try dragging the divider between the sidebar and main content. The sidebar should resize smoothly. What are the minimum and maximum sizes?
Window Configuration
The wails.json file controls window appearance and behavior:
| Setting | Default | What it does |
|---|---|---|
| width | 1024 | Default window width in pixels |
| height | 768 | Default window height in pixels |
| minWidth | 800 | Minimum window width (cannot shrink smaller) |
| minHeight | 600 | Minimum window height |
| frameless | true | Hide OS title bar for custom UI |
| fullscreen | false | Start in fullscreen mode |
Challenge: Configure the Window
Change the window size to 1200x800 in wails.json. Set the minimum size to 900x650. Restart the app — is the window larger? Try resizing it below the minimum — does it stop?
What You Learned
- How frameless windows work — hiding the OS title bar for a custom look
- Building a custom title bar with Wails runtime window controls
- Making windows draggable with
--wails-draggableCSS property - Auto-generated sidebar navigation from resources
- The dark theme system with CSS custom properties
- Using shadcn/ui components in desktop apps
- Resizable panels for IDE-style layouts
- Window configuration in wails.json
Challenge: Full Customization Challenge
Customize your app in four ways: (1) Change the accent color from purple to a color of your choice. (2) Modify the title bar text to show your app's name. (3) Add a new icon to the sidebar for a section called "Settings". (4) Adjust the default window size to 1280x720 in wails.json.
Challenge: Theme Experiment
Create a second theme by duplicating the CSS variables with different colors (e.g., a light theme or a blue-tinted dark theme). Add a button in the title bar that toggles between themes by swapping CSS classes on the root element. Does the entire app update?
Enjoying the course?
Help us grow — star us on GitHub, subscribe on YouTube, and follow on LinkedIn.