Skip to content

Theme Tokens

Kitsune is themed entirely through CSS custom properties. There is no styling runtime. Components consume var(--kit-*); the cascade applies whatever you set.

Terminal window
pnpm add @atheory-ai/kitsune-theme

defaultTheme from @atheory-ai/kitsune-theme:

TokenDefaultUsed by
--kit-color-accentoklch(57% 0.18 248)Buttons, focus, accents
--kit-color-accent-contrastwhiteText/icon on accent surfaces
--kit-color-surfaceCanvasComponent backgrounds
--kit-color-textCanvasTextBody text
--kit-color-border(system)Component borders
--kit-color-mutedGrayTextDescriptions, secondary text
--kit-color-erroroklch(58% 0.18 25)Errors, destructive actions
--kit-color-successoklch(60% 0.16 150)Success toasts, confirms
--kit-focus-ringHighlightFocus outlines
--kit-radius-sm0.375remButtons, inputs, small surfaces
--kit-radius-md0.5remCards, dialogs
--kit-space-10.25remSmallest spacing
--kit-space-20.5remSmall spacing
--kit-space-30.75remDefault spacing
--kit-space-41remLarger spacing
--kit-button-bgvar(--kit-color-accent)<kit-button> background
--kit-button-colorvar(--kit-color-accent-contrast)<kit-button> foreground
--kit-button-bordervar(--kit-color-accent)<kit-button> border

Components use system colors (Canvas, CanvasText, Highlight) as fallbacks so high-contrast and forced-color modes adapt automatically.

The default theme is a value, not a stylesheet. Convert it to CSS at build time:

import { defaultTheme, themeToCss } from '@atheory-ai/kitsune-theme'
console.log(themeToCss(defaultTheme))
// :root {
// --kit-color-accent: oklch(57% 0.18 248);
// ...
// }

Or apply imperatively at runtime:

import { applyTheme, defaultTheme } from '@atheory-ai/kitsune-theme'
applyTheme(defaultTheme)

applyTheme sets each token as an inline style on document.documentElement (or any element you pass).

import { applyTheme, defineTheme } from '@atheory-ai/kitsune-theme'
const brand = defineTheme({
name: 'kitsune-dark',
tokens: {
'--kit-color-accent': 'oklch(72% 0.18 285)',
'--kit-color-surface': 'oklch(20% 0 250)',
'--kit-color-text': 'oklch(96% 0 0)',
'--kit-color-border': 'oklch(35% 0 250)',
'--kit-radius-sm': '0.5rem',
},
})
applyTheme(brand)

defineTheme is a type-checked constructor — it ensures every token name starts with --kit-.

Swap themes with another applyTheme. Tokens replace. Anything not present in the new theme falls back to the previous values or the component’s hard-coded defaults.

const button = document.querySelector('button')
document.querySelector('#brand-toggle').addEventListener('click', () => {
applyTheme(currentTheme.name === 'kitsune-dark' ? defaultTheme : brand)
})

For per-section themes, scope applyTheme to a specific element instead of the document root:

applyTheme(brand, document.getElementById('marketing-section'))

Kitsune does not currently ship a layered stylesheet, but the convention is:

@layer kit.reset, kit.tokens, kit.base, kit.components, kit.utilities, app.overrides;
@layer kit.tokens { :root { /* tokens */ } }
@layer kit.components { /* component defaults */ }
@layer app.overrides { /* your overrides win */ }

Layers let your app code override Kitsune defaults without specificity wars and without !important.

Most components expose their theme as --kit-<component>-<property>:

:host {
background: var(--kit-button-bg, var(--kit-color-accent));
color: var(--kit-button-color, var(--kit-color-accent-contrast));
border: 1px solid var(--kit-button-border, var(--kit-color-accent));
}

To override a single button:

.danger {
--kit-button-bg: var(--kit-color-error);
--kit-button-color: white;
--kit-button-border: var(--kit-color-error);
}
<kit-button class="danger">Delete</kit-button>

You don’t need to pierce shadow DOM. The custom properties cascade through the shadow boundary.

Custom properties on the host become available inside the shadow DOM:

<kit-button style="--kit-button-bg: oklch(50% 0.2 25)">Crimson</kit-button>

Use this sparingly — most theming should live in your stylesheet, not inline.

The simplest dark-mode strategy doesn’t need a theme switcher at all:

:root {
color-scheme: light dark;
--kit-color-surface: light-dark(white, oklch(20% 0 250));
--kit-color-text: light-dark(oklch(20% 0 250), oklch(96% 0 0));
}

The browser picks based on prefers-color-scheme. Works in every browser as of 2024.

For manual override:

[data-theme="dark"] {
color-scheme: dark;
--kit-color-surface: oklch(20% 0 250);
/* ... */
}