Skip to content

Accessibility

The color system has one load-bearing accessibility rule, and it is not about individual values: a foreground token is only legible on the surface family it was made for. The scheme does not remap semantic tokens when it flips — it changes which family you reference. Pair the wrong two and you get a contrast failure no token value can rescue. Everything else on this tab is the ordinary WCAG 2.1 AA baseline.

Foregrounds come in families named after the surface they belong on. Match them:

Surface Text and icons Brand accents
background/neutral/*, surface/neutral/* foreground/neutral/* foreground/primary/on-neutral/*
background/contrast/*, surface/contrast/* foreground/contrast/* foreground/primary/on-contrast/*

A neutral foreground on a contrast surface is the single most common colour defect in a review, and it is what the naming exists to prevent. If a pairing isn’t in the table, it isn’t a pairing.

Within a family, emphasis → default → subtle step down in weight while staying readable body text. muted does not: it is the disabled step. Never use a *-muted foreground for text a user is expected to read, and never reach for it to make a label look quieter — that’s subtle.

The *-dim variants are opacity-based, so their effective contrast depends on whatever sits behind them. Check them against the actual surface rather than assuming the family guarantee holds.

Purely decorative colour is exempt from all three, but “decorative” means the interface loses nothing if it disappears — a divider that marks a real boundary, or a border that is the only thing defining an input, is not decorative.

Anything colour communicates must also be carried by text, an icon, or shape. An error field that is only red, a required label that is only orange, and a chart that distinguishes series only by hue all fail for colour-blind users and in greyscale. Source: WCAG 1.4.1 Use of Color.

Use the scheme-aware utilities and you get the pairing contract for free. .fg-default, .background-subtle, .border-emphasis and the rest each hold both halves of the pair and resolve whichever the surrounding .tng-scheme selects, so the same markup is correct on a white card and in a dark hero. The same is true of the scheme-aware components listed on the Scheme tab.

The failure mode is bypassing that. Reaching past the utility to the token pins the text to one family, and it goes unreadable the moment the block is dropped inside .is-contrast:

<!-- Don't: pinned to the neutral family -->
<p
class="tng-text-body"
style="color: var(--tng-color-foreground-neutral-subtle)"
>
Quieter label
</p>
<!-- Do: the utility carries both families and resolves the active scheme -->
<p class="tng-text-body fg-subtle">Quieter label</p>

Every scheme-aware utility works this way — it declares the neutral and the contrast token as a pair and lets the surrounding scheme pick. A raw semantic token in your own CSS is only safe where you declare both halves the same way; one of them on its own is a latent bug, not a shortcut.

Contrast is a property of a rendered pairing, not of a token, so it is not something the token layer can assert on your behalf. Two things are worth checking by hand: any *-dim or opacity-based colour, and any pairing you introduced outside the table above.