Theming
A theme in Bones is a short CSS file, not a fork. Every design decision is a custom property prefixed --ub-, and components only ever read the semantic ones (--ub-bg, --ub-accent, ...), never raw palette steps. No provider, no config file; the switches are HTML attributes.
Color steps
The eight color scales run 50 through 950 on one shared lightness ladder, so any step swaps cleanly across hues; gray runs a slightly darker ladder tuned for backgrounds. Gray is the neutral default. Red, orange, green, and blue back the feedback roles (danger, warning, success, info); violet, teal, fuchsia, and rose are secondary colors for badges and accents. Raw steps are for themes to reference; components never read them directly.
Dark mode
Add data-theme="dark" to <html> or any subtree (a .dark class works too). There is no JavaScript involved; it's a CSS scope. To default to the system preference without a flash, run this before paint:
<script>
try {
var t = localStorage.getItem("ub-theme");
if (t === "dark" || (!t && matchMedia("(prefers-color-scheme: dark)").matches))
document.documentElement.setAttribute("data-theme", "dark");
} catch (e) {}
</script>Radius: rounded or pill
Interactive elements read --ub-radius-control, which is 8px by default. Set data-radius="pill" anywhere to make every control inside fully rounded.
<html data-radius="pill">Your own accent
The neutral default uses near-black as the accent. Give it a color by overriding four tokens:
:root {
--ub-accent: oklch(0.55 0.2 260);
--ub-accent-hover: oklch(0.5 0.2 260);
--ub-accent-contrast: white;
--ub-ring: oklch(0.7 0.15 260);
}Prebuilt accent themes
Each of the eight color scales ships as a ready accent theme. One import recolors everything that means "primary action" in both modes:
import "@usebones/tokens/themes/blue.css";Full themes
A full theme replaces every color role, not just the accent. It ships inert and activates with the same attribute that drives dark mode. matrix is a quirky one to try: green phosphor, sharp corners.
import "@usebones/tokens/themes/matrix.css";
<html data-theme="matrix">Creating a theme
A theme is a plain CSS file imported after index.css. Accent theme: override the four action tokens in both modes (copy themes/blue.css). Full theme: scope every color role under your own data-theme value:
[data-theme="my-theme"] {
color-scheme: dark; /* or light: pick one, a full theme replaces both */
/* backgrounds */
--ub-bg: ...;
--ub-bg-subtle: ...;
--ub-bg-muted: ...;
--ub-bg-muted-hover: ...;
--ub-surface: ...;
--ub-overlay: ...;
/* text: primary, secondary, tertiary, disabled */
/* borders: border, border-strong */
/* interactive: accent, accent-hover, accent-contrast, ring */
/* feedback: danger (+hover/contrast), success, warning, info */
/* elevation: shadow-sm/md/lg */
/* optional: the radius scale, like matrix does */
}The override reference below is the checklist. Keep primary text at 4.5:1 contrast, leave danger red so destructive reads as destructive, and skip --ub-surface-glass; it derives from your surface automatically. A theme file works from anywhere; to ship one in @usebones/tokens, open a PR adding a file under css/themes/.
Styling component states
Below the token layer, every part has a stable class (ub-button, ub-select-trigger) and exposes state as data attributes (data-checked, data-highlighted, data-invalid). Each component page lists its hooks under "Styling states".
.ub-combobox-item[data-highlighted] {
background: var(--ub-accent);
color: var(--ub-accent-contrast);
}Density is a token override
Because components read the size tokens for their default size, remapping them in a scope makes everything inside compact by default, no prop drilling:
.dense-section {
--ub-font-size: var(--ub-font-size-compact);
--ub-control-height: var(--ub-control-height-compact);
--ub-icon-size-default: var(--ub-icon-size-compact);
}The size tokens themselves are covered in Sizes.
Scoped themes
Tokens cascade, so any override can be scoped to a subtree: a sidebar that's always dark, a marketing section with a different accent. Wrap it in a class and override tokens there.
Override reference
Everything a theme can change, in one place. Colors list their light and dark defaults; the rest apply to both modes.
| Token | Light | Dark | Controls |
|---|---|---|---|
--ub-bg | white | gray-950 | Page background |
--ub-bg-subtle | gray-50 | gray-900 | Wells, code blocks, panels |
--ub-bg-muted | gray-200 | gray-800 | Hovers, secondary buttons, active pills |
--ub-bg-muted-hover | gray-300 | gray-700 | One step deeper on hover |
--ub-surface | white | gray-900 | Cards and popups |
--ub-surface-glass | surface at 90% | surface at 90% | Translucent overlays, pairs with blur |
--ub-overlay | black 40% | black 60% | Dimming behind dialogs |
--ub-text-primary | gray-950 | gray-50 | Headings and body text |
--ub-text-secondary | gray-600 | gray-400 | Supporting text, labels |
--ub-text-tertiary | gray-400 | gray-500 | Hints, captions, placeholders |
--ub-text-disabled | gray-300 | gray-700 | Disabled text |
--ub-border | gray-200 | gray-800 | Hairlines and dividers |
--ub-border-strong | gray-300 | gray-700 | Inputs, select triggers, scrollbars |
--ub-accent | gray-900 | gray-50 | Primary actions, checked states |
--ub-accent-hover | gray-800 | gray-200 | Primary actions on hover |
--ub-accent-contrast | white | gray-950 | Text and icons on the accent |
--ub-ring | gray-400 | gray-500 | Focus rings |
--ub-danger | red-600 | red-500 | Destructive actions, invalid states |
--ub-danger-hover | red-500 | red-400 | Destructive actions on hover |
--ub-danger-contrast | white | white | Text on danger |
--ub-success | green-600 | green-500 | Positive feedback |
--ub-warning | orange-500 | orange-400 | Caution |
--ub-info | blue-600 | blue-400 | Informational |
--ub-tint-<hue>-bg / -text | hue-100 / hue-700 (gray 200 / 700) | hue-900 / hue-300 (gray 800 / 300) | Soft fill and text per palette hue; badges |
--ub-shadow-sm / md / lg | soft | stronger | Elevation scale for overlays |
--ub-z-overlay | 1000 | same | One layer for every floating part; raise above taller app chrome |
--ub-radius-xs to xl, full | 4 to 16px, 9999px | same | Container rounding steps (radii, not sizes); pill mode also rounds the sm, md, and lg steps further |
--ub-radius-control | 8px (pill: full) | same | Every interactive element |
--ub-font-size / -compact | 16px / 14px | same | The two text sizes |
--ub-control-height / -compact | 36px / 28px | same | The two control heights |
--ub-icon-size-default / -compact | 16px / 14px | same | The two icon sizes |
--ub-duration-fast / base / slow | 120 / 180 / 280ms | same | All motion; zero under reduced motion |
--ub-ease-out / in-out / spring | curves | same | Easings; spring for thumbs and pills |