Design values come from quebi tokens, never from literals
No arbitrary values (bg-[#f00], text-[13px]), no raw Tailwind palette scales (text-gray-500), no hex in style props. Use the quebi token that means the thing — and if a value is genuinely domain-mandated, record it as an exception with its justification.
Tier 3errorlintno-hardcoded-design-values
Layout is yours. Appearance is the library's.
What this catches
An agent that cannot recall a token name writes the value it can see: `bg-[#0ea5e9]`, `text-gray-500`. It renders identically today, which is exactly why nobody catches it, and it stops following the theme the moment the theme moves.
Why
A token is a promise that one edit changes every site of a colour, radius, or elevation. A literal opts out of that promise silently: bg-[#0b0f14] looks identical to bg-quebi-bg today and stops matching the day the theme moves, and nothing fails when it does.
Raw palette scales are the same defect wearing a nicer name. text-gray-500 is not a token — it is a hardcoded value with a Tailwind alias, it has no light/dark behaviour of its own, and it is why a page ends up with four almost-identical greys. quebi's tokens (quebi-fg-muted, quebi-fg-subtle, quebi-line) resolve per theme; the palette scales do not.
This tier is where an escape hatch has to exist rather than be pretended away. Some values are mandated by something outside the design system — a regulator's colour scale, a partner's brand mark. Those stay literal, and the rule records where and why, so a real carve-out is not indistinguishable from sloppiness.
The spacing and type scales are tokens too — p-4 and text-sm are fine. It is colour, radius, elevation, and off-scale sizes that must resolve to quebi tokens.
import { Badge } from "@/components/badge"<Badge intent="info">Beta</Badge>
Three literals and a Tailwind default radius, all to rebuild something the library ships. Tier 3 violations are usually tier 2 violations that went one step further.
Same pixels today, and the only version that follows the theme. Note that text-white is a literal too: quebi is dark-first but not dark-only, and quebi-fg is what flips in light mode.
The exception, and why it is one
Real code from src/components/energy-class-badge.tsx
Don't
band: { // Approximating the EU energy-label scale with palette tokens A: "bg-emerald-600 text-white", B: "bg-lime-500 text-black",}
Do
band: { // Official EU energy-label scale (Regulation (EU) 2017/1369, dark-green A -> // red G). These colours ARE the label, not quebi brand colours: a token that // shifted with the theme would make the chip wrong, not just off-brand. A: "bg-[#00843d] text-quebi-fg", B: "bg-[#4caf30] text-black",}
The literal is right here and the token is wrong — which is exactly why the exception has to be written down next to the rule instead of argued case by case in review.
How to check this
Add one of these to your project and the rule holds without anyone having to remember it — including the agent writing half the JSX. The exceptions below are already applied, so a documented carve-out will not be reported.
What it will and will not catch: The check reads .tsx/.jsx only, so a hex in a stylesheet slips past — pair it with a CSS-side check everywhere except the file that defines your theme. A value mandated from outside the design system is not always a whole file you can except, so the last snippet shows how to claim that carve-out inline.
Biome — GritQL plugin
biome
Biome has no built-in rule for this one, so it ships as a GritQL plugin. Save it as ui-lib-rules/no-hardcoded-design-values.grit and add that path to `plugins` in your biome.jsonc. Its documented exceptions are compiled in as `$filename` guards, because Biome's overrides do not scope plugins.
// Design values come from quebi tokens, never from literals// AUTO-GENERATED from https://ui-lib.quebi.de/api/rules/no-hardcoded-design-values.json — do not edit by hand.//// https://ui-lib.quebi.de/rules/no-hardcoded-design-valueslanguage js;or { string(), JsxString() } as $value where { $value <: r".*(?:\[#[0-9a-fA-F]{3,8}\]|\[[0-9]+(?:px|rem|em)\]|\b(?:bg|text|border|ring|fill|stroke|from|via|to)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-[0-9]{2,3}\b).*", // documented exception: components/ui/** not $filename <: r".*components/ui/.*", // documented exception: src/components/** not $filename <: r".*src/components/.*", // documented exception: src/components/energy-class-badge.tsx not $filename <: r".*src/components/energy-class-badge\.tsx$", // documented exception: components/ui/energy-class-badge.tsx not $filename <: r".*components/ui/energy-class-badge\.tsx$", register_diagnostic( span = $value, message = "Hardcoded design value. Use a quebi token: colours -> bg-quebi-*/text-quebi-*/border-quebi-line, radii -> rounded-quebi-{sm,md,lg}, elevation -> shadow-quebi-glow. Raw palette scales (text-gray-500) are hardcoded values too — they do not follow the theme. See https://ui-lib.quebi.de/rules/no-hardcoded-design-values", severity = "error" )}
ripgrep — no setup at all
ripgrep
Finds candidates for review in any repo, linter or not. Coarser than the Biome check: it reads lines, not syntax, so expect false positives and treat a clean run as weaker evidence than a clean lint run.
One exception on this rule is a judgement call, so it cannot be a path. Biome's suppression syntax has a slot for the reason — fill it, because that note is what makes the carve-out reviewable instead of invisible.
{/* biome-ignore plugin: Third-party brand marks (e.g. a provider's logo colour in an integration tile) — A partner's brand colour is not ours to theme. */}
Enforcing more than this one rule? Take the whole config instead of collecting snippets.
Exceptions
Carve-outs are part of the rule, not a way around it. Each one is already an ignore glob in the checks above, so the cases listed here need no disable comment — and a case that is not listed is one to argue for, not to silence.
The ui-lib component source itself, wherever you pasted it (components/ui/**)
The components resolve palette scales deliberately — a danger state on red-500, a Badge intent on emerald-500 — and they are not yours to re-token. Lint the code you write; the vendored source is the library's problem, and it is where a value like this has already been argued about.
energy-class-badge.tsx — the seven A-G band colours
The EU energy-label colour scale is specified by regulation. The colours are domain-semantic — they identify the efficiency band the way the letter does — so they must not move with the quebi theme. The surrounding chip (radius, font, neutral fallback, text colours) is on quebi tokens, and the letter is always rendered as text so colour is never the sole signal.
Third-party brand marks (e.g. a provider's logo colour in an integration tile)
A partner's brand colour is not ours to theme. Keep it literal, next to a comment naming the brand, and keep everything around it on tokens.