Colors
Every color token, in each mode, with the contrast it is checked against.
The sliders set --tc-radius and --tc-density on this page only. The mode switch is in the top bar.
The three layers
A component never names a color. It names a role, and the role resolves differently in each mode.
The palette is the Tailwind v4 oklch ramp. The semantic layer maps each role to a shade, and @theme inline turns each one into a utility class.
/* palette - never referenced by a component */
--color-blue-600: oklch(54.6% 0.245 262.881);
/* semantic - what a component means */
--tc-primary-solid: var(--color-blue-600);
--tc-primary-on-solid: var(--color-white);
/* utility - what Tailwind generates */
@theme inline {
--color-primary-solid: var(--tc-primary-solid);
}
/* what you write */
className="bg-primary-solid text-primary-on-solid"Every component uses the semantic layer only, so a new mode never needs a component change.
Every class
One class per role. These are the names components use, and the ones you use beside them.
The six tones
Every tone has the same set of roles, so a Badge, an Alert and a Button all use a tone in the same way.
Shown in the page's mode. Each row is a live variable, not a picture of one.
Where each color is used
One screen, every role labelled.
bg-pagebg-surfacetext-bodyChoose how this project deploys.text-mutedbg-primary-solidborder-linebg-primary-softPreviewDevelopmentbg-surface-raisedbg-surface: The top bar. A box sitting on the page.border-line: The hairline under the bar and around the card.text-body: Titles and normal text.text-muted: The grey description line.bg-primary-solid+text-primary-on-solid: The Save button.bg-primary-soft+text-primary-on-soft: The selected row in the dropdown.bg-surface-raised: The dropdown, floating above the card. One step lighter, with a shadow.bg-page: The whole screen, behind everything.
The three modes
Light, dark and ocean are three CSS blocks that set the same variable names. This site follows your system's mode until you pick one.
Ocean is light mode with a cyan primary and a pill radius. A custom mode needs nothing more than one CSS block.
[data-theme='dark'] {
--tc-page: var(--color-neutral-950);
--tc-surface: var(--color-neutral-900);
--tc-surface-raised: var(--color-neutral-800);
--tc-body: var(--color-neutral-50);
--tc-muted: var(--color-neutral-400);
--tc-line: var(--color-neutral-800);
/* neutral-400 not neutral-500: on a raised card neutral-500 measured only 3.19:1. */
--tc-line-strong: var(--color-neutral-400);
--tc-overlay: oklch(0 0 0 / 0.7);
--tc-neutral-solid: var(--color-neutral-100);
--tc-neutral-on-solid: var(--color-neutral-950);
--tc-neutral-on-soft: var(--color-neutral-200);
--tc-primary-on-soft: var(--color-blue-400);
--tc-success-on-soft: var(--color-emerald-400);
--tc-warning-on-soft: var(--color-amber-400);
--tc-danger-on-soft: var(--color-red-400);
--tc-info-on-soft: var(--color-sky-400);
/* At the light percentages every dark tint measured 1.01-1.03 on --tc-surface. */
--tc-primary-mix: 24%;
--tc-neutral-mix: 12%;
--tc-success-mix: 24%;
--tc-warning-mix: 14%;
--tc-danger-mix: 22%;
--tc-info-mix: 24%;
/* Near-white in dark, so it steps toward the middle from the other side. */
--tc-neutral-hover: var(--color-neutral-200);
--tc-neutral-active: var(--color-neutral-300);
/* neutral-400 not neutral-500: on a raised card neutral-500 measured only 3.19:1. */
--tc-scrollbar-thumb: var(--color-neutral-400);
--tc-scrollbar-track: transparent;
}What the contrast check enforces
Contrast is checked on every change to the library, and a failure blocks the change. It uses CSS Color 4 gamut mapping, because some shades sit outside sRGB.
Add your own theme
Add one CSS block and set data-theme="brand" on the element it covers. A mode can change knobs as well as colors.
--tc-primary-solid must also set --tc-primary-on-solid, or its label can fall below 4.5:1 on the new fill./* after the three imports */
[data-theme="brand"] {
color-scheme: dark;
--tc-page: var(--color-slate-950);
--tc-surface: var(--color-slate-900);
--tc-surface-raised: var(--color-slate-800);
--tc-body: var(--color-slate-50);
--tc-muted: var(--color-slate-400);
--tc-line: var(--color-slate-800);
--tc-line-strong: var(--color-slate-600);
--tc-primary-solid: var(--color-teal-500);
--tc-primary-on-solid: var(--color-slate-950);
--tc-primary-on-soft: var(--color-teal-300);
--tc-radius: 1rem;
}ThemeScript stores only light, dark and ocean, so a custom mode goes on a section, or on html in a layout that does not render ThemeScript.