AA vs AAA: what these levels actually mean and why your disabled button is failing both ended on a specific instruction: when a color fails AAA in one context, document the exception on the token itself, not around it. That instruction assumes the color reaching the screen is a token to begin with, something like var(--color-accent) a browser can resolve, not a hex string typed once into a stylesheet and copied everywhere it was needed after. Most design systems don't reach that assumption safely. A token file gets created, a handful of components get built against it, and the rest of the codebase keeps shipping literals that happen to match the tokens by coincidence, not by reference. This is the audit for finding exactly where that gap is, and the setup that keeps it from reopening.
What a design token actually is
A design token is a named variable that stores one design decision once, so every platform that needs that decision reads it from a single source instead of re-implementing it. The term has a specific origin: Jina Anne coined it in 2014 while building the Salesforce Lightning Design System, where the same color, spacing, and type values had to reach CSS, iOS, and Android, each with its own syntax for a variable. In a 2019 interview she made a point worth keeping in front of any team adopting the pattern: design tokens are a methodology, not a file format. Calling them "just variables" is like calling responsive design "just media queries," and it misses the actual discipline underneath both.
That discipline now has a shared exchange format behind it. The W3C's Design Tokens Community Group published the first stable version of its Design Tokens Format Module in October 2025, giving tools that previously spoke their own proprietary token formats a common file shape to read and write. None of that infrastructure does anything on its own if the values it produces never make it into the stylesheet a browser actually loads. That gap, between a token existing and a token being used everywhere the design decision it represents shows up, is what the rest of this article is about.
Where hardcoded values hide, even with a token file
2.1 Copied straight from the inspector panel
The most common source has nothing to do with engineering discipline. Someone opens the inspect panel on a component, copies a hex string, and it goes into the stylesheet exactly as copied: #f3b02e instead of var(--y). It renders identically to the token-driven version. Nothing about the visual output tells anyone it isn't the token, which is exactly why it survives review.
2.2 Inline styles and third-party embeds
A hand-written style="" attribute skips the token layer entirely, and so does one written by JavaScript at runtime, which is what most third-party widgets do: a newsletter form, a chat bubble, a calendar embed. These render on brand-colored pages while reading zero of that brand's tokens, because the script that built them has no access to the host page's CSS custom properties unless someone explicitly wires it up.
2.3 Ported legacy CSS
A redesign rarely starts from zero. Old rules get carried into the new stylesheet wholesale during migration, hex values included, because rewriting every declaration by hand the same week as a rebrand isn't realistic. The intention is always to come back and replace them later. The audit in section 3 is what makes that intention checkable instead of aspirational.
2.4 SVG source files
An icon exported with fill="#f3b02e" baked into the path itself carries a hardcoded value that lives outside the CSS file the rest of the audit checks. fill="currentColor", or a CSS custom property set on the <svg> tag, lets the icon inherit whatever color context surrounds it. A hardcoded fill doesn't, and it stays invisible to anyone scanning only .css files for the problem.
The five-command audit
None of the four sources above show up by inspecting a rendered component, they all look correct in the browser. They show up by searching the source directly. The five commands below are the same audit this project runs on every codebase before anything ships, adapted to any project with a :root token block and a .css / .html / .svg split.
-
grep -rn 'style="' --include=*.html .Catches: every inline style attribute, including the ones added to "just get it done" during a deadline.
-
grep -rnE '#[0-9a-fA-F]{3,8}\b' --include=*.css assets/css/ | grep -v ':root'Catches: raw hex values living outside the token definition block, the ones a browser resolves directly instead of through a custom property.
-
grep -rnE 'fill="#|stroke="#' --include=*.svg assets/img/Catches: hardcoded color baked into icon source files, the blind spot from section 2.4.
-
grep -rn 'rgba(' assets/css/ | grep -v 'var(--'Catches: one-off transparency values invented on the spot instead of referencing an existing token like
--accent-dim. -
grep -rnE '\b[0-9]{2,}px\b' assets/css/ | grep -v 'var(--'Catches: pixel values outside the spacing and radius scale. This one needs a human pass, not every hit is wrong (a 1-2px border is legitimate CSS), so treat it as a shortlist for review, not an automatic fail.
Five commands, five different places the same problem hides. No linter, build step, or plugin required. All five run in a terminal in under thirty seconds.
Making it a gate, not a cleanup
A grep audit run once fixes today's debt and says nothing about tomorrow's pull request. Two tools turn the same patterns above into something that fails a build instead of waiting to be remembered.
Stylelint's color-no-hex rule rejects any hex color value at lint time, the same values command 2 above finds by hand. declaration-property-value-disallowed-list goes further and lets a team disallow specific property and value patterns project-wide, for example blocking any color or background declaration matching /^#/ outside the token file itself. Wired into CI, either rule turns a hardcoded value into a failed pull request instead of a missed review comment.
The other half of the problem, the same value needing to reach CSS, iOS, and Android from one definition, is what Style Dictionary was built for. Amazon open-sourced it in 2017 as a build system that takes a single token source file and outputs it in whatever format each platform's stylesheet actually needs. A token that only exists in a design tool's export panel isn't distributed, it's documented. A build step like Style Dictionary is what makes "one value, everywhere" true past the website, not just true in principle.
Tokens without a hierarchy are renamed hex
Passing the audit in section 3 answers whether a value is a token. It doesn't answer whether the token system is built correctly. Nathan Curtis's 2016 breakdown of token architecture at EightShapes set out the tiering most systems still reach for today: a primitive token stores a raw value, a semantic token names the job that value does, and a component only ever reads the semantic layer, never the primitive directly.
Raw value
#f3b02e
Primitive token
--y: #f3b02e;
Semantic token
--accent: var(--y);
Component
.btn-primary { background: var(--accent); }
Four layers, one value. A component only ever reads the semantic layer. It never needs to know the raw value underneath it exists.
Skip that tier and a token system becomes a find-and-replace tool wearing a design system's name. A flat list of tokens named after their own value, a raw hex renamed into a variable but still describing color instead of role, means a rebrand is still a manual pass through every file that used it. Nothing about the naming tells anyone which of forty color tokens is safe to repoint and which one something else quietly depends on.
This site's own token file has that seam visible on purpose. globals.css defines primitives like --y: #f3b02e and a block of semantic aliases underneath them, --accent: var(--y), --text-primary: var(--t1), so a component styled against .btn-primary { background: var(--accent) } never has to know or care that the raw value is amber. That indirection is the entire point: change what --accent points to once, and every component reading it changes with it, without a single component file being touched.
The swatch below is the same idea, live. One box reads a token, the other reads the literal it currently matches. Click a button and only one of them moves.
Reads var(--demo-accent)
token
Reads #f3b02e
hardcoded literal
Both boxes render the identical color right now. That similarity is exactly what makes a hardcoded value invisible in review, until the brand accent changes and only one box responds.
The two-minute check
No linter required, no build step, just a text editor and a browser before a component ships.
-
Search the file for
#. Anything outside the:rootblock is not a token, regardless of what color it happens to match. -
Open dev tools, select the element, and check the computed value's source. A value traced back to a CSS custom property is a token. A value that's just there, with no custom property attached, isn't.
-
Check every
<svg>in the file.fillandstrokeshould readcurrentColoror avar(--...), never a literal hex. -
If the same raw value shows up three times in one file with nothing tying the instances together, that's the signal it should have been a token from the first line, not the third copy-paste.
Sources
- Design Tokens Community Group, W3C. Design Tokens Specification Reaches First Stable Version. w3.org/community/design-tokens
- Smashing Magazine. Smashing Podcast Episode 3 With Jina Anne: What Are Design Tokens? smashingmagazine.com/2019/11/smashing-podcast-episode-3
- Curtis, Nathan. EightShapes. Tokens in Design Systems. eightshapes.com/articles/tokens-in-design-systems
- Stylelint. color-no-hex. stylelint.io/user-guide/rules/color-no-hex
- Stylelint. declaration-property-value-disallowed-list. stylelint.io/user-guide/rules/declaration-property-value-disallowed-list
- Style Dictionary. Open-source build system for design tokens. github.com/style-dictionary/style-dictionary