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
| Surface | Files | What it does |
|---|---|---|
| Settings UI | app/routes/settings/sections/AppearanceSection.tsx | Renders light/dark/system buttons and accent swatches. |
| Theme helpers | app/utils/theme-settings.ts | Applies root dark class, root data-accent, and titlebar IPC sync. |
| CSS variables | app/styles/globals.css | Defines Tailwind v4 tokens, shadcn/ui variables, dark mode, and accent overrides. |
| Window CSS | app/styles/window.css | Defines titlebar, drag region, frame background, and scrollbar colors. |
| Native launch colors | lib/main/app.ts | Creates the BrowserWindow with light/dark background and Windows titlebar colors. |
| Settings schema | lib/conveyor/schemas/app-schema.ts | Validates 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-menuGenerated components should land under app/components/ui and use
app/styles/globals.css for variables.
Customizing the palette
For a brand pass:
- Change the default brand primary in
:rootand.dark. - Update
--ringto the same hue family or a deliberate focus color. - Keep
--primary-foregroundreadable against the new primary. - If you keep accent choices, update
ACCENT_SWATCHESinAppearanceSection.tsxto match the CSS values. - If you add or remove accents, update
ACCENT_COLORSandaccentColorSchemainapp-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
themeandaccentColor. - 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