Theming

Dark, light, and system themes with accent colors and native chrome sync.

The starter has two independent appearance controls: light/dark/system theme and accent color. Both are persisted in the main-process settings store and applied in the renderer through CSS variables, with Windows titlebar chrome kept in sync.

What ships

SurfaceFilesWhat it does
Settings UIapp/routes/settings/sections/AppearanceSection.tsxRenders light/dark/system buttons and accent swatches.
Theme helpersapp/utils/theme-settings.tsApplies root dark class, root data-accent, and titlebar IPC sync.
CSS variablesapp/styles/globals.cssDefines Tailwind v4 tokens, shadcn/ui variables, dark mode, and accent overrides.
Window CSSapp/styles/window.cssDefines titlebar, drag region, frame background, and scrollbar colors.
Native launch colorslib/main/app.tsCreates the BrowserWindow with light/dark background and Windows titlebar colors.
Settings schemalib/conveyor/schemas/app-schema.tsValidates theme and accentColor values crossing IPC.

Theme modes

theme is one of:

type ThemePreference = 'light' | 'dark' | 'system'

applyThemePreference() resolves system with window.matchMedia('(prefers-color-scheme: dark)'), toggles the root dark class, and calls window.conveyor.window.windowSetTitlebarDark(isDark).

On Windows, window-set-titlebar-dark updates BrowserWindow.setTitleBarOverlay so titlebar background and symbol colors match the renderer. At launch, lib/main/app.ts also resolves the stored setting through nativeTheme before the renderer is ready, avoiding a mismatched first frame.

Accent colors

accentColor is one of:

type AccentPreference = 'default' | 'blue' | 'violet' | 'green' | 'rose' | 'amber'

default removes data-accent and keeps the brand primary. Other values set document.documentElement.dataset.accent, which activates these selectors in app/styles/globals.css:

:root[data-accent='violet'] {
  --primary: hsl(262 83% 58%);
  --primary-foreground: hsl(0 0% 100%);
  --ring: hsl(262 83% 58%);
}

The accent layer only overrides --primary, --primary-foreground, and --ring, so semantic colors such as --background, --card, --muted, and --destructive stay stable across products.

Tailwind and shadcn/ui

The project uses Tailwind v4 with CSS variables:

@import 'tailwindcss';
@variant dark (&:where(.dark, .dark *));
@import 'tw-animate-css';

@theme maps app variables to Tailwind color tokens, while shadcn/ui components read the same variables through bg-background, text-foreground, border-border, bg-primary, and related classes.

components.json is configured for shadcn/ui new-york style, tsx: true, CSS variables enabled, neutral base color, and the lucide icon library. Add components with the existing aliases:

pnpm dlx shadcn@latest add dialog dropdown-menu

Generated components should land under app/components/ui and use app/styles/globals.css for variables.

Customizing the palette

For a brand pass:

  1. Change the default brand primary in :root and .dark.
  2. Update --ring to the same hue family or a deliberate focus color.
  3. Keep --primary-foreground readable against the new primary.
  4. If you keep accent choices, update ACCENT_SWATCHES in AppearanceSection.tsx to match the CSS values.
  5. If you add or remove accents, update ACCENT_COLORS and accentColorSchema in app-schema.ts, then add a settings-store migration if existing users need a fallback.

Verify

Run the app and check:

  • Light, dark, and system modes switch from Settings → Appearance.
  • Accent swatches update primary buttons and focus rings.
  • Restarting preserves theme and accentColor.
  • Windows titlebar overlay colors match the active mode.
  • Tray icon colors still adapt to the OS theme.

Then run:

pnpm lint:check && pnpm typecheck && pnpm test

On this page