Color

The color system separates reusable palette values from the roles those colors perform in an interface.

Primitive tokens provide the underlying color values. Semantic tokens assign those values to reusable interface roles such as backgrounds, text, borders, feedback, and focus. Component tokens define colors for specific components and states.

Use semantic color tokens when styling general interface elements and component tokens when styling shared components. Primitive palette tokens should not normally be used directly in component styles.

The system supports light and dark color schemes through scheme-aware semantic and component tokens.

Token structure

Color tokens are organized into three layers:

  1. Primitive tokens define the available color palettes.
  2. Semantic tokens describe how color is used in an interface.
  3. Component tokens define color decisions for a specific component.
/* Primitive */
--color-neutral-500: oklch(55.6% 0 0);

/* Semantic */
--color-focus: var(--color-neutral-500);

/* Component */
--color-input-border-focus: var(--color-focus);

This separation allows palette values, reusable interface roles, and component decisions to change independently.

Primitive colors

Primitive tokens describe a color value without assigning it an interface purpose.

Neutral

The neutral palette provides values from near-white to near-black.

--color-neutral-50: oklch(98.5% 0 0);
--color-neutral-100: oklch(97% 0 0);
--color-neutral-200: oklch(92.2% 0 0);
--color-neutral-300: oklch(87% 0 0);
--color-neutral-400: oklch(70.8% 0 0);
--color-neutral-500: oklch(55.6% 0 0);
--color-neutral-600: oklch(43.9% 0 0);
--color-neutral-700: oklch(37.1% 0 0);
--color-neutral-800: oklch(26.9% 0 0);
--color-neutral-900: oklch(20.5% 0 0);
--color-neutral-950: oklch(14.5% 0 0);

Neutral colors provide the underlying values for backgrounds, text, borders, disabled states, focus indicators, and other interface roles.

Brand

The brand palette provides the primary color family for a project.

Foundations provides the middle range required by the current component and semantic tokens:

--color-brand-400
--color-brand-500
--color-brand-600
--color-brand-700
--color-brand-800

The palette name remains consistent between projects, while its values can be changed to match the project’s visual identity.

Additional brand steps can be introduced when a project requires them. Do not add unused palette steps merely to create a complete numeric range.

Components should not depend on a particular brand value unless the decision belongs specifically to that component.

Red

The red palette provides a partial scale for destructive actions, errors, and danger states.

--color-red-400: oklch(70.4% 0.191 22.216);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-red-700: oklch(50.5% 0.213 27.518);
--color-red-800: oklch(44.4% 0.177 26.899);
--color-red-900: oklch(39.6% 0.141 25.723);

Not every primitive step needs to be referenced by a semantic or component token. Intermediate values remain available when a project or future component requires them.

Green

The green palette currently provides the values required by success states.

--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-800: oklch(44.8% 0.119 151.328);

The darker value is used in the light color scheme and the lighter value in the dark color scheme so the semantic success color remains visible against its surrounding surfaces.

White and black

--color-white: oklch(100% 0 0);
--color-black: oklch(0% 0 0);

These tokens provide explicit endpoints where a palette step is not appropriate.

Semantic colors

Semantic tokens describe reusable roles a color performs. They allow the underlying palette values to change without requiring changes throughout the interface.

Backgrounds

--color-bg-dark: light-dark(
  var(--color-neutral-100),
  var(--color-neutral-950)
);

--color-bg: light-dark(
  var(--color-neutral-50),
  var(--color-neutral-900)
);

--color-bg-light: light-dark(
  var(--color-white),
  var(--color-neutral-800)
);

These tokens create three ordered background levels:

  • --color-bg-dark provides the darkest background level.
  • --color-bg provides the default page or surface background.
  • --color-bg-light provides the lightest background level.

The names describe their relationship to one another, not whether they belong exclusively to the light or dark color scheme.

Text

--color-text
--color-text-muted
--color-text-link
  • --color-text is the default foreground color.
  • --color-text-muted is used for supporting or lower-emphasis text.
  • --color-text-link is used for text links.

Muted text remains readable text. It should not be used to imitate disabled content or hide information through insufficient contrast.

Borders and dividers

--color-divider
--color-border
--color-border-hover
  • --color-divider separates adjacent content.
  • --color-border defines a subtle default boundary.
  • --color-border-hover provides a stronger boundary for hover states.

A component that requires a stronger boundary for accessibility or interaction can define a component-specific border token instead of changing the general border role.

Feedback and focus

--color-success: light-dark(
  var(--color-green-800),
  var(--color-green-500)
);

--color-danger: light-dark(
  var(--color-red-700),
  var(--color-red-400)
);

--color-focus: var(--color-neutral-500);
  • --color-success identifies successful outcomes.
  • --color-danger identifies errors, invalid values, and destructive outcomes.
  • --color-focus provides the shared color used by focus indicators.

Success and danger use different primitive values between light and dark color schemes to preserve appropriate contrast.

Focus is a separate semantic role. Do not derive it from muted text or another unrelated role merely because they happen to use a similar color.

Color must not be the only method used to communicate success, danger, validity, selection, or focus.

Component colors

Component tokens define colors used by a particular component.

--color-button-*
--color-input-*

They provide a stable component-level API while allowing the underlying semantic or primitive values to change independently.

For example, primary Button colors use the brand palette directly:

--color-button-primary-bg: var(--color-brand-600);
--color-button-primary-bg-hover: var(--color-brand-700);
--color-button-primary-bg-active: var(--color-brand-800);
--color-button-primary-text: var(--color-neutral-50);

Input borders use component-specific values because an interactive control boundary has different requirements from a general interface border:

--color-input-border: var(--color-neutral-500);

--color-input-border-hover: light-dark(
  var(--color-neutral-600),
  var(--color-neutral-400)
);

--color-input-border-focus: var(--color-focus);
--color-input-border-invalid: var(--color-danger);

Component styles should consume their component tokens rather than reaching directly into primitive palettes.

This allows a component’s appearance to be adjusted without replacing raw color values throughout its stylesheet.

Complete component-token references belong in the documentation for the relevant component.

Color schemes

The system supports light and dark color schemes.

:root {
  color-scheme: light dark;

  &[data-theme="light"] {
    color-scheme: light;
  }

  &[data-theme="dark"] {
    color-scheme: dark;
  }
}

Without an explicit data-theme value, the browser selects between the supported schemes according to the user’s preference.

Setting data-theme="light" or data-theme="dark" restricts the document to the selected scheme.

Scheme-aware tokens use light-dark():

--color-text: light-dark(
  var(--color-neutral-800),
  var(--color-neutral-100)
);

The first value is used by the light color scheme. The second value is used by the dark color scheme.

Components do not need separate light and dark selectors when their colors resolve through scheme-aware tokens.

Derived colors

The system can derive colors from existing tokens when a separate fixed palette value is unnecessary.

--color-button-secondary-bg-hover:
  color-mix(in oklch, var(--color-text) 8%, transparent);

--color-button-secondary-bg-active:
  color-mix(in oklch, var(--color-text) 14%, transparent);

Use color-mix() for effects that should remain connected to another token, such as subtle hover and active backgrounds.

The resulting color must still be tested in every supported color scheme.

Use a derived color only when its relationship to the source token is intentional. Do not use color functions to avoid introducing a semantic or component token that the interface genuinely needs.

Usage

Use semantic tokens for general interface styles

.card {
  color: var(--color-text);
  background-color: var(--color-bg-light);
  border: 1px solid var(--color-border);
}

Use component tokens inside shared components

.button {
  color: var(--color-button-primary-text);
  background-color: var(--color-button-primary-bg);
  border-color: var(--color-button-primary-border);
}

Avoid direct primitive usage in components

/* Avoid */
.button {
  background-color: var(--color-brand-700);
}

/* Use */
.button {
  background-color: var(--color-button-primary-bg);
}

Direct primitive usage creates an undocumented relationship between the component and the palette.

Primitive tokens remain appropriate when:

  • defining semantic tokens
  • defining component tokens
  • displaying the palette itself
  • creating intentionally decorative colors with no semantic role

Adding a color token

Before adding a token, determine which layer it belongs to.

Add a primitive token when

  • the required color value does not exist in a current palette
  • an existing palette needs another reusable step
  • a genuinely separate color family is required

A primitive does not need to be used immediately by a semantic or component token if it forms a useful part of an established palette.

Add a semantic token when

  • a color has a reusable interface purpose
  • several components or layouts need the same role
  • the role may need different values between color schemes

Add a component token when

  • the decision belongs to one component
  • the component requires independently configurable states
  • changing the value globally would affect unrelated uses

Avoid creating aliases that do not introduce a meaningful level of control.

Accessibility

Color accessibility depends on the final foreground and background combination, not on either token by itself.

Test:

  • default text against every background on which it appears
  • muted text against its actual backgrounds
  • link text in default and hover states
  • button text against default, hover, active, and disabled backgrounds
  • input text, placeholder text, borders, and invalid states
  • focus indicators against the colors immediately around them
  • all combinations in both light and dark color schemes

For WCAG 2.2 Level AA, normal text requires a contrast ratio of at least 4.5:1 and large text requires at least 3:1.

Visual information required to identify interface components and states must provide at least 3:1 contrast against adjacent colors where the Non-text Contrast criterion applies. Focus indicators must remain clearly visible in every supported state and color scheme.

Do not communicate information through color alone. Pair color with text, an icon, a shape change, or another visible indicator where the meaning would otherwise be lost.