Theming
Every colour in the library resolves through one token layer. That is what makes the toggle in the header work without a single dark: class, and it is why re-theming is a handful of CSS declarations rather than a fork.
How it works
Components style themselves with semantic utilities — bg-surface, text-foreground-muted, border-error-muted — which Tailwind resolves to custom properties declared in @xui/core/styles/theme.css. Swap the values and every component follows.
Never reach for the raw palette
bg-white and text-zinc-900 cannot follow the active theme. If you find yourself writing a dark: variant, that is the signal a raw colour crept in — there is a token for it. Switching themes
Put .dark or [data-theme='dark'] on <html>. With neither present the theme follows prefers-color-scheme. Both selectors work on any element, so a subtree can render in the opposite theme — useful for a preview pane.
<html class="dark">…</html>
<!-- or, on any subtree -->
<div data-theme="light">…</div>To avoid a flash of the wrong theme, decide before first paint rather than at bootstrap. This site uses exactly this:
// index.html, before the app bundle
const stored = localStorage.getItem('xui-docs-theme');
const dark = stored ? stored === 'dark' : matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(dark ? 'dark' : 'light');Re-theming
Override tokens after the import. The darker, lighter, subtle, muted and emphasis steps of each intent are derived with oklch() and color-mix(), so one declaration re-colours the whole ramp — hover, border, badge and callout included.
@import 'tailwindcss';
@import '@xui/core/styles/theme.css';
:root {
--primary: var(--color-violet-600);
--surface-inset: var(--color-zinc-50);
}Do it with the swatches instead
Control density
Anything you click or type into takes its height and horizontal padding from one scale, so a button, an input and a date field on the same row line up. Override the middle step and the library re-tunes with it.
:root {
--control-height-sm: 1.5rem; /* 24px */
--control-height-md: 1.875rem; /* 30px — the default step */
--control-height-lg: 2.5rem; /* 40px */
--control-padding-sm: 0.5rem;
--control-padding-md: 0.75rem;
--control-padding-lg: 1rem;
}Token reference
89 tokens, read out of theme.css at build time. Swatches render in whichever theme you are viewing.
Surfaces
Backgrounds, in depth order. `background` is the page, `surface` sits on it, `surface-raised` on that.
--backgroundcolor--mutedcolor--muted-foregroundcolor--surfacecolor--surface-insetcolor--surface-overlaycolor--surface-raisedcolor--surface-sunkencolorText
Foreground colours by emphasis, not by shade.
--foregroundcolor--foreground-mutedcolor--foreground-on-emphasiscolor--foreground-subtlecolorBorders
Divider and outline colours.
--bordercolor--border-mutedcolor--border-strongcolorIntents
The six meanings. Each has a derived ramp, so re-colouring one declaration re-colours all of it.
--errorcolor--error-darkercolor--error-emphasiscolor--error-foregroundcolor--error-lightercolor--error-mutedcolor--error-subtlecolor--infocolor--info-darkercolor--info-emphasiscolor--info-foregroundcolor--info-lightercolor--info-mutedcolor--info-subtlecolor--primarycolor--primary-darkercolor--primary-emphasiscolor--primary-foregroundcolor--primary-lightercolor--primary-mutedcolor--primary-subtlecolor--secondarycolor--secondary-darkercolor--secondary-emphasiscolor--secondary-foregroundcolor--secondary-lightercolor--secondary-mutedcolor--secondary-subtlecolor--successcolor--success-darkercolor--success-emphasiscolor--success-foregroundcolor--success-lightercolor--success-mutedcolor--success-subtlecolor--warningcolor--warning-darkercolor--warning-emphasiscolor--warning-foregroundcolor--warning-lightercolor--warning-mutedcolor--warning-subtlecolorState
Focus, selection and the hover/active overlays, so interactive states look the same everywhere.
--active-overlaycolor--focuscolor--hover-overlaycolor--linkcolor--link-hovercolor--selectioncolorElevation
The shadow scale. Separate from Tailwind’s `shadow-*` so re-tuning depth never moves `shadow-md`.
--elevation-0shadow--elevation-1shadow--elevation-2shadow--elevation-3shadow--elevation-4shadow--elevation-overlayshadowMotion
Durations and easings.
--duration-base--duration-fast--duration-slowOther
Everything else the theme declares.
--chart-1color--chart-2color--chart-3color--chart-4color--chart-5color--chart-6color--chart-7color--chart-8color--control-height-lg--control-height-md--control-height-sm--control-padding-lg--control-padding-md--control-padding-sm--emphasis-l-max--emphasis-l-min--emphasis-shift