Theming | Kuzan | Primitives Docs

Theming

Customize colors, dark mode, and design tokens in kuzan

Theme model

kuzan is theme-driven via CSS custom properties. The runtime color scheme is controlled by:

  • A class on <html> (class="dark" or omitted for light)
  • An optional data-theme="system" attribute (follows the OS preference)
  • An optional <ThemeInit /> script that reads localStorage['kui-theme'] and applies the saved choice on first paint

There is no framework-level config — the choice lives in your src/routes/layout.koze.

Color schemes

Dark mode

<html lang="en" class="dark">

Dark background with light text. Optimized for low-light environments.

Light mode

<html lang="en">

White background with dark text. Default when no class is set.

System preference

Follows the user's OS color scheme via prefers-color-scheme:

<html lang="en" data-theme="system">

Runtime theme switching

Use the ThemeToggle component to let users switch themes:

<script>
  import ThemeToggle from '@kuratchi/kuzan/theme-toggle.koze';
</script>

<ThemeToggle />

The toggle:

  • Adds/removes the .dark class on <html>
  • Saves the preference to localStorage under "kui-theme"
  • Syncs all toggle instances on the page

Pair it with <ThemeInit /> in <head> so the saved theme applies before the page paints — see Preventing FOUC.

Corner radius variants

kuzan reads radius from a data-radius attribute on <html>:

<html lang="en" class="dark" data-radius="default">

Supported values:

  • data-radius="default" — standard rounded corners (0.375rem - 0.75rem)
  • data-radius="none" — square corners (0px radius)
  • data-radius="full" — pill-shaped / fully rounded corners (9999px)

Omit the attribute for the default behavior.

CSS variables

All theme values are exposed as CSS custom properties. Override them in your own stylesheet:

Color tokens

:root {
  /* Backgrounds */
  --kui-bg: #ffffff;
  --kui-bg-muted: #f9fafb;
  --kui-bg-elevated: #ffffff;

  /* Foreground */
  --kui-fg: #09090b;
  --kui-fg-muted: #71717a;

  /* Borders & focus */
  --kui-border: #e4e4e7;
  --kui-ring: #18181b;

  /* Primary brand color */
  --kui-primary: #584cd9;
  --kui-primary-fg: #ffffff;
  --kui-primary-weak: color-mix(in srgb, #584cd9 12%, transparent);
  --kui-primary-strong: #4338ca;

  /* Semantic colors */
  --kui-destructive: #ef4444;
  --kui-destructive-fg: #ffffff;
  --kui-success: #22c55e;
  --kui-success-fg: #ffffff;
  --kui-warning: #f59e0b;
  --kui-warning-fg: #ffffff;
}

.dark {
  --kui-bg: #09090b;
  --kui-bg-muted: #18181b;
  --kui-fg: #fafafa;
  --kui-fg-muted: #a1a1aa;
  --kui-border: #27272a;
  --kui-primary: #818cf8;
  /* ... */
}

Spacing tokens

:root {
  --kui-spacing-xs: 0.25rem;
  --kui-spacing-sm: 0.5rem;
  --kui-spacing-md: 1rem;
  --kui-spacing-lg: 1.5rem;
  --kui-spacing-xl: 2rem;
}

Radius tokens

:root {
  --kui-radius-sm: 0.375rem;
  --kui-radius-md: 0.5rem;
  --kui-radius-lg: 0.75rem;
}

Shadow tokens

:root {
  --kui-shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
  --kui-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
  --kui-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.06);
  --kui-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1), 0 4px 6px rgba(0, 0, 0, 0.05);
}

Timing tokens

:root {
  --kui-duration-fast: 100ms;
  --kui-duration-base: 150ms;
}

Customizing colors

Override CSS variables in src/app.css after the theme import:

/* src/app.css */
@import '@kuratchi/kuzan/styles/theme.css';

:root {
  /* Change primary brand color */
  --kui-primary: #10b981;
  --kui-primary-fg: #ffffff;
  --kui-primary-strong: #059669;

  /* Adjust background */
  --kui-bg: #fafafa;
  --kui-bg-muted: #f4f4f5;
}

.dark {
  --kui-primary: #34d399;
  --kui-primary-fg: #09090b;
}

The framework auto-injects src/app.css into the app shell — no <link> tag needed.

Using with Tailwind CSS

kuzan composes with Tailwind via the standard PostCSS / Tailwind v4 pipeline. Install:

npm install -D @tailwindcss/vite

Add to your vite.config.ts:

import tailwindcss from '@tailwindcss/vite';
import { koze } from '@kuratchi/koze/vite';

export default defineConfig({
  plugins: [
    koze(),
    tailwindcss(),
    cloudflare({ viteEnvironment: { name: 'ssr' } }),
  ],
});

Then import both from src/app.css:

@import 'tailwindcss';
@import '@kuratchi/kuzan/styles/theme.css';
@source './routes/**/*.koze';

You can use Tailwind utility classes alongside kuzan components:

<Card title="Dashboard" class="max-w-2xl mx-auto">
  <p class="text-sm text-gray-600">Custom Tailwind styling</p>
</Card>

Font customization

The default font stack:

:root {
  --kui-font-sans: 'Inter', 'Segoe UI', system-ui, -apple-system,
                   BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
}

Override it in src/app.css:

:root {
  --kui-font-sans: 'Your Font', system-ui, sans-serif;
}

body {
  font-family: var(--kui-font-sans);
}

Preventing FOUC

To prevent a flash of unstyled content when the page loads, use the <ThemeInit /> component in your layout's <head>:

<script>
  import ThemeInit from '@kuratchi/kuzan/theme-init.koze';
</script>

<!doctype html>
<html lang="en" class="dark">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>My App</title>
    <ThemeInit />
  </head>
  <body>
    <slot></slot>
  </body>
</html>

<ThemeInit /> reads localStorage['kui-theme'] and applies the saved class to <html> before the page paints.

Next steps