Skip to main content

Tokens · Token families

Contrast check

Check your own color overrides against the contrast LUZ promises, with one command or in the page, before they reach a user.

Files: bin/luz-tokens.js

Run it

LUZ keeps its own colors readable in light, dark and high contrast. The moment you override a token, that promise is yours. check-contrast reads the --luz-* declarations of your stylesheet, puts them over LUZ's tokens as the browser would, and measures the same pairs the component pages list. It needs Node 20 or later and nothing else. luz-check, the command of @luz-ds/check, runs the same check on the stylesheets you name, next to its rules for the HTML and CSS of a page.

Shell
npx @luz-ds/tokens@0.3.0 check-contrast my-theme.css

Name your files in the order the page loads them. Use --json for a machine-readable report, and --strict to make the warnings fail too.

Output
✗ light, high-contrast: --luz-text-secondary on surface-page is 2.8:1, needs 4.5:1 (text)
    --luz-text-secondary: #999 is set in my-theme.css:2
✗ contrast: 5 pairs fail (9 of 378 measurements) in light, dark, high-contrast

What it checks

WCAG 2.x relative luminance, the contrast of WCAG 2.2 level AA. APCA is not used, because it is not part of the standard.

KindMinimumWhat it protects
text
4.5:1
Text and links on every surface they sit on, status text on its tint, text on a filled button, syntax colors on a code block
disabled-text
3:1
Disabled text. LUZ's own rule: it stays readable, and the state has a cue besides color
ui
3:1
The focus ring, field borders, outlines and the played part of a progress bar

Pairs are checked in light, dark and high contrast, and in each [data-theme="name"] your file defines, as name and name/light, name/dark and name/high-contrast. Translucent colors are placed over the surface below them, as in the page. A warning is an aim LUZ sets itself, stricter than WCAG, such as the fill of a checked box against the page: it is shown and never fails the check unless you pass --strict.

What it reads

  • Selectors: :root, html, [data-theme="x"] and [data-mode="y"], alone or together. A later rule wins over an earlier one of the same specificity, and a more specific selector wins over a later one. A :root override, loaded after LUZ, also reaches dark and high contrast unless that mode sets the token again: set a token per mode.
  • At-rules: @media (prefers-color-scheme: dark) and light, and @layer and @supports, taken as true. The order of layers is not modeled.
  • Values: #rgb, #rrggbb with or without alpha, rgb() and hsl() in either syntax, white, black, transparent, and var() chains.
  • Everything else is reported, not skipped: a class selector, another media query or a rule it did not understand is listed as ignored. A color it cannot read, such as color-mix() or oklch(), counts as a failure, because a pair that was not measured is not a pair that passed.

Exit codes

CodeMeaning
0
Every pair meets its minimum
1
A pair fails, or a color could not be read
2
The command was used wrongly, or a file could not be read

In your build

The command fails the step on a failure, so one line is enough.

package.json
{
  "scripts": {
    "check:contrast": "luz-tokens check-contrast src/theme.css"
  }
}

Try it in the page

With JavaScript on, you can paste your overrides here and run the same check. Nothing leaves the page. Without it, use the command above: it does the same.

Paste the --luz-* tokens you override.