Skip to content
TopClass UI

Colors

Every color token, in each mode, with the contrast it is checked against.

Radius (rem)0.5
Density1

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.

Class, Variable, What it is for, Light, Dark, Ocean
SwatchClassVariableWhat it is forLightDarkOcean
bg-page--tc-pageThe page behind everythingwhiteNot measured: a surface or a decorative lineneutral-950Not measured: a surface or a decorative linewhiteNot measured: a surface or a decorative line
bg-surface--tc-surfaceA recessed or striped surfaceneutral-50Not measured: a surface or a decorative lineneutral-900Not measured: a surface or a decorative lineneutral-50Not measured: a surface or a decorative line
bg-surface-raised--tc-surface-raisedA card, a popup, an inputwhiteNot measured: a surface or a decorative lineneutral-800Not measured: a surface or a decorative linewhiteNot measured: a surface or a decorative line
text-body--tc-bodyBody textneutral-95018.96:1, needs 4.5:1neutral-5014.48:1, needs 4.5:1neutral-95018.96:1, needs 4.5:1
text-muted--tc-mutedSecondary textneutral-6007.47:1, needs 4.5:1neutral-4005.83:1, needs 4.5:1neutral-6007.47:1, needs 4.5:1
border-line--tc-lineA decorative hairlineneutral-200Not measured: a surface or a decorative lineneutral-800Not measured: a surface or a decorative lineneutral-200Not measured: a surface or a decorative line
border-line-strong--tc-line-strongA control boundary, must reach 3:1neutral-5004.53:1, needs 3:1neutral-4005.83:1, needs 3:1neutral-5004.53:1, needs 3:1
outline-ring--tc-ringThe focus ringblue-8008.47:1, needs 3:1blue-4005.73:1, needs 3:1cyan-8006.94:1, needs 3:1
bg-primary-solid--tc-primary-solidA filled controlblue-6005.26:1, needs 4.5:1blue-6005.26:1, needs 4.5:1cyan-7005.28:1, needs 4.5:1
text-primary-on-solid--tc-primary-on-solidText on that fillwhite5.26:1, needs 4.5:1white5.26:1, needs 4.5:1white5.28:1, needs 4.5:1
bg-primary-soft--tc-primary-softA tint, for a chip or a hovered row12% primary-solid into the page7.52:1, needs 4.5:124% primary-solid into the page6.25:1, needs 4.5:112% primary-solid into the page6.14:1, needs 4.5:1
text-primary-on-soft--tc-primary-on-softText and icons on that tintblue-8007.52:1, needs 4.5:1blue-4006.25:1, needs 4.5:1cyan-8006.14:1, needs 4.5:1

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.

Role, What it is for, Light, Dark, Ocean
SwatchRoleWhat it is forLightDarkOcean
primary/solidA filled control with a white labelblue-6005.26:1, needs 4.5:1blue-6005.26:1, needs 4.5:1cyan-7005.28:1, needs 4.5:1
primary/softA tint, for a chip or a hovered row12% primary-solid into the page7.52:1, needs 4.5:124% primary-solid into the page6.25:1, needs 4.5:112% primary-solid into the page6.14:1, needs 4.5:1
primary/on-softText and icons on that tintblue-8007.52:1, needs 4.5:1blue-4006.25:1, needs 4.5:1cyan-8006.14:1, needs 4.5:1
success/solidA filled control with a white labelemerald-7005.37:1, needs 4.5:1emerald-7005.37:1, needs 4.5:1emerald-7005.37:1, needs 4.5:1
warning/solidIts label is near-black, not whiteamber-5009.23:1, needs 4.5:1amber-5009.23:1, needs 4.5:1amber-5009.23:1, needs 4.5:1
danger/solidDestructive actionsred-6004.76:1, needs 4.5:1red-6004.76:1, needs 4.5:1red-6004.76:1, needs 4.5:1
info/solidNeutral informationsky-7005.85:1, needs 4.5:1sky-7005.85:1, needs 4.5:1sky-7005.85:1, needs 4.5:1
neutral/solidFlips with the modeneutral-90017.91:1, needs 4.5:1neutral-10018.15:1, needs 4.5:1neutral-90017.91:1, needs 4.5:1

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.

An example screen, each part labelled with the class that paints it
bg-page
TopClass UIbg-surface
Project settingstext-bodyChoose how this project deploys.text-muted
bg-primary-solidborder-line
Productionbg-primary-softPreviewDevelopmentbg-surface-raised
  • bg-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.

topclassui/tokens.css
[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.

What, Threshold, Measured against
WhatThresholdMeasured against
Text, including a tone text on its own tint4.5:1The surface it sits on
line-strong, ring and icons3:1All three surfaces, not only the page
A soft tint1.06 stepAll three surfaces
lineexemptDecorative hairline, WCAG 1.4.11

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.

A mode that sets --tc-primary-solid must also set --tc-primary-on-solid, or its label can fall below 4.5:1 on the new fill.
app/globals.css
/* 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.
NextIcons