Theming

How UI Lab's token system works — color families, shade ranges, dark mode inversion, and dynamic accent customization.

Theming

UI Lab is built on a semantic token system backed by OKLCH — a perceptually-uniform color space. Every color decision routes through CSS custom properties, so swapping themes or customizing the accent is a one-line change.

Consumers usually own the token layer in their app's theme.css. With Tailwind v4, the normal setup is:

@import "tailwindcss";
@import "./theme.css";
@import "ui-lab-components/styles.css";

theme.css defines your app tokens and mode rules. ui-lab-components/styles.css consumes those active --color-* tokens. You do not need a tailwind.config.ts content extension step for this integration.

Import this stylesheet once, before any other app or component module, from your application entry point — see Installation for why import order and placement matter.

If you're starting from ui-lab-theme-onyx instead of a fully app-owned theme, import it right after tailwindcss and before your own theme.css:

@import "tailwindcss";
@import "ui-lab-theme-onyx/styles.css";
@import "./theme.css";
@import "ui-lab-components/styles.css";

Foundational token contract

A fully app-owned theme.css (no ui-lab-theme-onyx import) must define every token component CSS reads, not just colors. Some foundational tokens — like --border-width-base — are read directly by component CSS without a fallback. Leaving one undefined can make the dependent property compute to an invalid value, which is different from "wrong color": it can render as a missing border or a square corner where a rounded one was expected.

:root {
  --border-width-base: 1px;
}

ui-lab-theme-onyx already defines the foundational token contract, so apps that import it don't need to repeat this.


How the token pipeline works


Color families

Each family has a bounded shade range:

FamilyShade rangeDirectionUse for
background500–950500 = lightest surfacePage, card, container backgrounds
foreground50–40050 = lightest textText, borders, icons, labels
accent50–600600 = strongestPrimary actions, CTAs, links
success50–600600 = strongestConfirmations, positive states
danger50–600600 = strongestErrors, destructive actions
warning50–600600 = strongestCautions, pending states
info50–600600 = strongestNeutral information

Dark mode inversion

There is no dark: prefix in UI Lab. Shades invert automatically when the theme switches:

/* Same class works in both modes */
.card {
  background-color: var(--background-700);
  color: var(--foreground-300);
}
/* Dark:  background-700 → deep dark surface,  foreground-300 → light text  */
/* Light: background-700 → mid-light surface,  foreground-300 → medium text */
{/* No dark: variant needed — shade inversion handles it */}
<div className="bg-background-700 text-foreground-300 border border-foreground-100">
  Works in both light and dark mode
</div>

Applying tokens

To make Tailwind utilities point at your live token values, map them in @theme inline inside your app-owned theme.css:

:root {
  --background-700: oklch(21% 0.005 51.4);
  --foreground-300: oklch(73% 0.02 51.4);
}

@theme inline {
  --color-background-700: var(--background-700);
  --color-foreground-300: var(--foreground-300);
}

Tailwind classes

{/* Surfaces */}
<div className="bg-background-600">Page background</div>
<div className="bg-background-700">Card surface</div>
<div className="bg-background-800">Elevated / hover state</div>

{/* Text */}
<p className="text-foreground-300">Body text</p>
<p className="text-foreground-200">Secondary text</p>
<p className="text-foreground-100">Muted / caption</p>

{/* Primary actions */}
<button className="bg-accent-600 text-foreground-50 hover:bg-accent-500">
  Primary button
</button>

{/* Semantic states */}
<div className="bg-success-50 border border-success-200 text-success-600">
  Success message
</div>
<div className="bg-danger-50 border border-danger-200 text-danger-600">
  Error message
</div>

CSS variables

.my-component {
  background: var(--background-700);
  color: var(--foreground-300);
  border: 1px solid var(--foreground-100);
}

.my-component:hover {
  background: var(--background-800);
}

Customizing the accent color

The accent family is designed for dynamic customization. Override it in your app-owned theme.css:

:root {
  --accent-50:  oklch(97% 0.012 262);
  --accent-100: oklch(93% 0.030 262);
  --accent-200: oklch(86% 0.065 262);
  --accent-300: oklch(74% 0.110 262);
  --accent-400: oklch(62% 0.145 262);
  --accent-500: oklch(52% 0.170 262);
  --accent-600: oklch(44% 0.190 262);
}

All components that use accent-* tokens pick up the change automatically — no component code changes needed.


Common mistakes

WrongCorrect
bg-whitebg-background-500
text-gray-900text-foreground-400
bg-zinc-100bg-background-600
bg-background-50bg-background-500 — min shade is 500
text-foreground-500text-foreground-400 — max shade is 400
bg-accent-700bg-accent-600 — max shade is 600
border-red-500border-danger-500

Troubleshooting

Broadly incorrect / reset-looking component styles

Symptom: components look unstyled or reset, as if Tailwind's base styles won over UI Lab's. Cause: CSS cascade layers registered in the wrong order — the global stylesheet was imported after an app/component module instead of before it, or ui-lab-theme-onyx / theme.css / ui-lab-components/styles.css were imported out of order. See Installation for the required order and entry-point placement.

Square Switch thumbs / invalid computed styles

Symptom: a component that should render with rounded corners (Switch's thumb is the most visible case) renders square, or a border is missing. Cause: a foundational token — most commonly --border-width-base — isn't defined by your theme.css. See Foundational token contract above.


Further reading

  • Installation — stylesheet setup and import order
  • Colors — full OKLCH palette reference with all shade values
  • Design Tokens — complete token reference for spacing, typography, and motion
  • Variables — all CSS custom properties and their defaults