Theming
Customize Velkin with design tokens, VuThemeProvider, and CSS variables.
Quick example
import { VuThemeProvider } from "@velkin/react/theme-provider";
import { VuButton } from "@velkin/react/button";
export function App({ children }: { children: React.ReactNode }) {
return (
<VuThemeProvider persist preference="system">
{children}
<VuButton variant="solid" color="primary">
Save
</VuButton>
</VuThemeProvider>
);
}Vue: @velkin/vue/theme-provider. Lit / HTML: import "@velkin/ui/theme-provider" and <vu-theme-provider persist>. Framework install steps: Installation.
How tokens work
You pass brand seeds (primary, radius, spacing, tint, …). The provider derives surfaces, borders, soft intents, and the full --vu-* scale. Prefer semantic tokens in your CSS over hard-coded hex.
| Layer | Examples |
|---|---|
| Global | --vu-space-*, --vu-radius-*, font tokens |
| Semantic | --vu-color-background, --vu-color-foreground, --vu-color-border, intent colors |
| Component | Appearance props and documented ::part() / cssProps on each component |
This docs site also ships design presets in the header palette control so you can preview the same components under different seeds. Light / dark preference stays a separate axis.
Using VuThemeProvider
Place the provider once near the root. Use a client / browser boundary if your framework requires it for interactive providers. Subpath import only — never the package root barrel.
- persist — stores preference in
localStorage(keytheme) when scope includes root - preference —
light,dark, orsystem(followsprefers-color-scheme) - scope —
root(default,<html>),host(provider element only), orboth
import { VuThemeProvider } from "@velkin/react/theme-provider";
export function App({ children }: { children: React.ReactNode }) {
return (
<VuThemeProvider persist preference="system">
{children}
</VuThemeProvider>
);
}Using CSS variables
Components resolve intents like primary and danger through the provider. In your own layout CSS, use the same tokens:
.page {
background: var(--vu-color-background);
color: var(--vu-color-foreground);
padding: var(--vu-space-6);
border-radius: var(--vu-radius-lg);
border: var(--vu-border-width) solid var(--vu-color-border);
}Explore live tokens in Studio, or open demos under Components and toggle the site theme switcher in the header.
Light and dark mode
Preference is what the user chose; mode is the resolved light or dark sheet. Use VuThemeSwitcher inside the provider so it reads context and updates preference.
import { VuThemeSwitcher } from "@velkin/react/theme-switcher";
// Cycle button (light → system → dark)
<VuThemeSwitcher type="button" variant="ghost" size="sm" />
// Explicit three-way control
<VuThemeSwitcher type="switch" variant="outline" size="sm" />Listen with onVuTheme. Payload is in e.detail (preference and resolved mode).
<VuThemeSwitcher
type="switch"
onVuTheme={(e) => {
console.log(e.detail.preference, e.detail.mode);
}}
/>Programmatically: call setPreference("dark") / togglePreference() on the provider element, or keep preference in React state and pass preference={pref}.
Avoid flash / first paint
On SSR or static HTML, inject theme tokens before interactive JS runs so light/dark does not flash. In React, use VuThemeHead and htmlThemeProps from @velkin/react/theme-head. Pass the same theme seeds to VuThemeProvider when branding.
import { VuThemeHead, htmlThemeProps } from "@velkin/react/theme-head";
import { VuThemeProvider } from "@velkin/react/theme-provider";
// Apply htmlThemeProps on the document <html> element in your app shell.
<html lang="en" {...htmlThemeProps}>
<head>
<VuThemeHead />
</head>
<body>
<VuThemeProvider persist>{/* app */}</VuThemeProvider>
</body>
</html>Vite, Next.js, Vue, and Lit wiring differ slightly — follow the matching guide under Installation.
Customizing a single component
For one-off tweaks, use documented CSS parts and cssProps — not undocumented shadow internals:
vu-button::part(base) {
/* only parts listed in the component docs */
}Full guidance: Styling. Prefer tokens and appearance props when the change should apply system-wide.
Custom theme seeds
Pass a theme object of brand seeds. Velkin derives the rest — you rarely set raw --vu-* by hand.
import { VuThemeProvider } from "@velkin/react/theme-provider";
const brand = {
primary: "#0d9488",
success: "#16a34a",
warning: "#d97706",
danger: "#dc2626",
radius: "0.75rem",
spacing: "0.25rem",
tint: "subtle", // "none" | "subtle" | "vivid"
fontSans: "ui-sans-serif, system-ui, sans-serif",
};
export function App({ children }: { children: React.ReactNode }) {
return (
<VuThemeProvider theme={brand} persist>
{children}
</VuThemeProvider>
);
}Per-mode overrides when light and dark need different seeds:
const theme = {
primary: "#6366f1",
light: { tint: "subtle" },
dark: { tint: "vivid", primary: "#818cf8" },
// Escape hatch for individual tokens after derivation
vars: {
light: { "--vu-space-4": "1.25rem" },
dark: { "--vu-shadow-surface": "0 1px 2px rgb(0 0 0 / 40%)" },
},
};- primary — brand accent (focus, links, primary buttons)
- success / warning / danger — intent colors; defaults apply if omitted
- radius / spacing — base values; every
--vu-radius-*and--vu-space-*step derives from them - tint — how strongly neutrals pick up the brand hue
- vibrantpalette — provider prop for higher-contrast soft intent foregrounds
- glass — provider prop for glass-style elevated surfaces (does not paint a page gradient)
Scoped themes
Nest a second provider with scope="host" to theme a panel without changing the whole document — useful for marketing strips, email previews, or Studio-style editors.
<VuThemeProvider persist>
<AppChrome />
<VuThemeProvider
scope="host"
preference="dark"
persist={false}
theme={{ primary: "#f97316" }}
>
<PreviewPane />
</VuThemeProvider>
</VuThemeProvider>Next steps
- Finish setup on Installation for your framework
- Styling — parts, cssProps, and tokens
- Browse theme-provider and theme-switcher API demos
- Design a full brand palette in Studio