Skip to content
onsystemv0.6.0

Rule

no-hardcoded-color

error by default

Colors must come from design tokens: no hex/rgb/oklch literals, arbitrary Tailwind colors or default-palette classes.

What it catches
Hex/rgb/hsl/oklch literals in classes (bg-[#ef4444], [color:red]), styles and color attributes (<svg fill>; on components, only hex and color functions in props without known values); Tailwind default-palette classes (bg-gray-100)
Why it matters
A hex value is a copy of a token that stops following it: when the theme changes, or in dark mode, the copy stays behind. Agents write them whenever a prompt names a color ("a red alert", "a green label"), and in the benchmark they were most of what the agents got wrong.

Example

What check_ui returned for this code against the demo design system when the site was built, from version 0.6.0.

What the agent wrote
<div className="rounded-md bg-gray-100 p-3 text-[#737373]">  Only owners can delete a workspace.</div>
After the fixthe rule’s own fixes
<div className="rounded-md bg-muted p-3 text-muted-foreground">  Only owners can delete a workspace.</div>
check_ui
  1. error1:28fix included

    bg-gray-100 is Tailwind's default palette, not a design-system color. Matches token muted (ΔE 0.004, same value as secondary, accent) → bg-muted.

  2. error1:44fix included

    Hardcoded color text-[#737373]. Matches token muted-foreground (exact match) → text-muted-foreground.

How the fix is chosen

The nearest token of the same hue whose role fits the utility (text-gray-500 → text-muted-foreground), or the variant that already applies it.

Every finding in the example carries its fix as text edits, which the agent applies, and so does eslint --fix. The code after the fix is those edits, applied.

Color matches under ΔE 0.02 count as the same color; under 0.1 the fix is offered; beyond that the message names the nearest token but leaves the choice to the agent. A fix is only offered for a token of the same hue (within 15°), or a gray for a gray, whether the color is a palette class, an arbitrary value or a literal: a pale color counts as a hue by its chroma relative to its lightness, so bg-[#fef2f2] is never swapped for a light gray that happens to be close. Status colors (destructive, warning, success, info and their foregrounds) are only offered for nearly the same hue (within 8°), never for a gray, so a brand-orange button does not become bg-destructive. Among the tokens that qualify, the one made for the utility wins: foreground and muted-foreground for text-*, fill-* and stroke-*; surfaces such as muted for bg-*; border, input and ring for border-* and ring-*. sidebar-* and chart-* tokens are only suggested in a sidebar or a chart (by file, enclosing component or classes), and so is any other family that at most one design-system file uses, such as syntax-highlighting colors or one screen's background, outside that file's components; dark: classes are compared with dark-mode values. Likewise, a spacing or radius step that is off by more than half the value (and more than 4px) is suggested but not auto-fixed. Rules that need tokens are skipped when the design system defines none of that category; a stylesheet that imports tailwindcss brings Tailwind's default spacing unit and radius scale, unless the theme resets that namespace (--spacing-*: initial, --radius-*: initial), in which case only the project's own steps are suggested. Syntax errors are reported as syntax, and so is code nested too deeply for TypeScript's parser (thousands of levels), which is then not checked.

Options

Its default severity is "error". Set it to "off", "warn" or "error" in onsystem.config.json, or as onsystem/no-hardcoded-color in the ESLint plugin.

Colors to accept anyway, as written in the class or the style: ["error", { "allow": ["#fff"] }].

onsystem.config.json
{  "rules": {    "no-hardcoded-color": "warn"  }}

The config file covers the rest, including overrides for folders.

Suppressing a finding

When a finding is intended, a comment turns the rule off for the next line and says why after --. Checked when the site was built: the comment hides the finding on the line below it, and the others stay.

With the comment
// onsystem-disable-next-line no-hardcoded-color -- why this one is intended<div className="rounded-md bg-gray-100 p-3 text-[#737373]">  Only owners can delete a workspace.</div>

onsystem-disable-next-line covers the next line, onsystem-disable-line its own, and /* onsystem-disable */ the rest of the file (at the top, the whole file). Without rule names they turn every rule off. A misspelled rule name is reported as a suppression warning, with the closest one, and suppresses nothing; syntax errors cannot be suppressed. For whole folders or kinds of files, such as legacy screens or generated code, use overrides in the config.