Courses/Grit Desktop/Custom UI & Theming
Course 3 of 5~30 min10 challenges

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

Frameless Window: A desktop window without the operating system's default title bar and borders. Instead of the standard Windows/macOS/Linux title bar with its minimize, maximize, and close buttons, you build your own with React. This gives your app a modern, custom look — like VS Code, Spotify, or Discord.

Grit desktop apps use frameless windows by default. The setting lives in wails.json:

wails.json (partial)
{
  "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.

1

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:

frontend/src/components/title-bar.tsx (simplified)
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>
  )
}
Window Controls: Buttons that control the desktop window — minimize (hide to taskbar), maximize (fill screen), and close (exit the app). In a frameless window, you build these yourself with React instead of relying on the operating system's built-in controls.

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
2

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:

Draggable region
{/* 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.

3

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:

Sidebar entries (auto-generated)
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).

4

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:

frontend/src/index.css (theme variables)
: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)
To change the entire color scheme of your app, just modify the CSS variables. Change--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.
5

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:

ComponentUsed for
ButtonPrimary actions, form submissions, navigation
InputText fields in forms
DialogModal dialogs for confirmations and forms
DataTableSortable, filterable data tables for listing resources
SelectDropdown selection (e.g., category picker)
CheckboxBoolean toggles (e.g., task done/not done)
TextareaMulti-line text input for descriptions

All components live in frontend/src/components/ui/ and are imported like:

Import example
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog'
6

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:

Resizable layout pattern
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.

7

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:

SettingDefaultWhat it does
width1024Default window width in pixels
height768Default window height in pixels
minWidth800Minimum window width (cannot shrink smaller)
minHeight600Minimum window height
framelesstrueHide OS title bar for custom UI
fullscreenfalseStart in fullscreen mode
8

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-draggable CSS 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
9

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.

10

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?