Skip to content

Theming

Every component reads CSS custom properties. Override them anywhere after the stylesheet and the whole set follows.

:root {
--primary: oklch(0.55 0.2 264);
--primary-foreground: oklch(0.98 0 0);
--radius: 0.5rem;
}
TokenUsed for
--background / --foregroundPage surface and its text
--card / --card-foregroundCard surface
--popover / --popover-foregroundDropdown menu surface
--primary / --primary-foregroundPrimary button, focus accents
--secondary / --secondary-foregroundSecondary button
--muted / --muted-foregroundDescriptions, placeholders
--accent / --accent-foregroundHover and focus states
--destructiveDanger surfaces and text — the surface is a wash of it, the text is it
--border / --input / --ringBorders, field borders, focus ring
--radiusBase corner radius; sm/md/lg/xl derive from it

Because the names match shadcn/ui, a palette generated for shadcn drops in unchanged.

Dark tokens live under the .dark class on <html>.

<html class="dark">

partialkit ships a toggle that persists the choice under pk-theme in localStorage:

<button class="btn btn-outline btn-icon" data-pk-theme aria-label="Toggle theme"></button>
ValueBehaviour
data-pk-themeFlips between light and dark
data-pk-theme="dark"Forces dark
data-pk-theme="light"Forces light
data-pk-theme="system"Clears the choice and follows the OS

Add this to <head> so the page does not flash before the runtime loads:

<script>
if (localStorage.getItem("pk-theme") === "dark"
|| (!localStorage.getItem("pk-theme") && matchMedia("(prefers-color-scheme: dark)").matches)) {
document.documentElement.classList.add("dark");
}
</script>

A pk:theme:change event fires on <html> with { theme, dark } whenever it changes.

axe-core runs against every documentation page in both modes on each build, so a token change that breaks WCAG AA fails CI.

Two exceptions are recorded rather than fixed, both places where shadcn/ui’s own values land under the bar:

PairingMeasuresWould need
--destructive text on a wash of itself3.98:1 light, 4.28:1 dark--destructive at oklch(0.50 …), which clears it at 4.95:1
--muted-foreground on a muted surface4.27:1 to 4.34:1a darker muted foreground

The second covers every place shadcn/ui pairs muted text with a muted surface: a Kbd, an avatar fallback, an avatar group count, and a description inside a checked choice card. All of them pass in dark mode.

Both ship shadcn/ui’s values rather than a fork. The exceptions are narrow — each names the rule and the markup it covers, and the second is bounded to a near miss, so muted text that is genuinely unreadable still fails the build. A test asserts the exceptions are still triggered, so they disappear the day shadcn/ui lifts the colours instead of quietly outliving their reason.

If your product needs AA on destructive text, one line gets you there:

:root {
--destructive: oklch(0.50 0.245 27.325);
}
Danger