Rules
Eight rules. Every finding says what to write instead.
check_ui for the agent, and check for CI and the Claude Code hook, run the same rules against the design system read from your source. This page is generated from version 0.3.1: each example below went through the real linter on the demo design system when the site was built. Try your own in the playground.
no-hardcoded-color
error by defaultColors must come from design tokens: no hex/rgb/oklch literals, arbitrary Tailwind colors or default-palette classes.
- 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) - Suggests
- 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 - 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.
<div className="rounded-md bg-gray-100 p-3 text-[#737373]"> Only owners can delete a workspace.</div><div className="rounded-md bg-muted p-3 text-muted-foreground"> Only owners can delete a workspace.</div>error1:28fix included
bg-gray-100is Tailwind's default palette, not a design-system color. Matches token muted (ΔE 0.004, same value as secondary, accent) →bg-muted.error1:44fix included
Hardcoded color
text-[#737373]. Matches token muted-foreground (exact match) →text-muted-foreground.
Option. Colors to accept anyway, as written in the class or the style: ["error", { "allow": ["#fff"] }].
no-hardcoded-spacing
warning by defaultPadding, margin and gap must use the spacing scale, not arbitrary px/rem values.
- Catches
- Arbitrary padding, margin and gap (
p-[13px],style={{ marginTop: 6 }}) - Suggests
- Nearest step on the spacing scale, keeping the sign; with Tailwind v4, any whole or half step of
--spacing(p-3,p-4.5,p-13) orp-px - Why it matters
- Off-scale padding and gaps make layouts drift a pixel at a time, and an arbitrary value hides which step was meant. When the value is on the scale, the fix is just its name. A warning by default, because a deliberate one-off is sometimes right.
<div className="flex items-center gap-[6px] px-[18px] py-[13px]"> <Badge>Beta</Badge></div><div className="flex items-center gap-1.5 px-4.5 py-3"> <Badge>Beta</Badge></div>warning1:35fix included
gap-[6px]is 6px, which is on the spacing scale: usegap-1.5.warning1:45fix included
px-[18px]is 18px, which is on the spacing scale: usepx-4.5.warning1:55fix included
Hardcoded spacing
py-[13px](13px) is off the scale. Nearest:py-3(12px).
Option. Values to accept anyway, as the class or the length: ["warn", { "allow": ["px-[18px]"] }].
no-hardcoded-radius
warning by defaultBorder radius must use radius tokens, not arbitrary values.
- Catches
- Arbitrary radius (
rounded-[7px],borderRadius: 14) - Suggests
- Nearest radius token (
rounded-sm), including Tailwind's default keys;rounded-full(or a pill token) for values far above the scale (rounded-[999px]) - Why it matters
- Corners are part of the visual language. An arbitrary radius looks almost right next to the components and never quite matches them. A warning by default.
<div className="rounded-[7px] border p-4"> <Badge className="rounded-[999px]">New</Badge></div><div className="rounded-sm border p-4"> <Badge className="rounded-full">New</Badge></div>warning1:17fix included
Hardcoded radius
rounded-[7px](7px) is off the scale. Nearest:rounded-sm(6px).warning2:21fix included
rounded-[999px](999px) is far above the radius scale, so it reads as fully rounded: userounded-full.
Option. Values to accept anyway, as the class or the length.
prefer-design-system-component
error by defaultUse the design-system component instead of the native element it wraps (<Button> over <button>).
- Catches
- Native elements a component wraps (
<button>,<input>,<dialog>), inferred from each component's props and markup - Suggests
- The root component and its import, preferring one that renders the element (
NativeSelectoverSelect); the rename is auto-fixed when it renders that element, takes its attributes or is mapped inelements - Why it matters
- A native
<button>skips what the design system'sButtoncarries: variants, focus styles, disabled states, and every fix the team made since. Agents write native elements with hand-rolled classes because that is what most code they learned from looks like.
<form className="flex gap-2"> <input type="email" placeholder="you@example.com" /> <button className="rounded-md bg-primary px-3 text-sm text-primary-foreground"> Invite </button></form><form className="flex gap-2"> <Input type="email" placeholder="you@example.com" /> <Button className="rounded-md bg-primary px-3 text-sm text-primary-foreground"> Invite </Button></form>error2:4fix included
Native <input> where the design system has <Input>. Use <Input> (import { Input } from "@/components/ui/input").
error3:4fix included
Native <button> where the design system has <Button>. Use <Button> (import { Button } from "@/components/ui/button"); its variants replace the custom classes.
Option. Native elements to accept anyway: ["error", { "allow": ["a"] }].
no-unknown-component
error by defaultComponents must exist in the design system: no invented components or dot-notation members.
- Catches
- Invented components, dot-notation members that do not exist (
<Card.Header>), typos - Suggests
- The flat part (
<CardHeader>) or closest name, auto-fixed only when that component is imported - Why it matters
- Agents mix up conventions between libraries:
<Card.Header>where the system exports flat parts, or a component that only exists in another kit. TypeScript reports it once the file compiles in the project;check_uireports it in a snippet the agent has not saved yet, and names the part to use.
<Card> <Card.Header> <Card.Title>Billing</Card.Title> </Card.Header></Card><Card> <CardHeader> <CardTitle>Billing</CardTitle> </CardHeader></Card>error2:4fix included
<Card.Header> does not exist: Card is composed from flat parts. Use <CardHeader> (import { CardHeader } from "@/components/ui/card").
error3:6fix included
<Card.Title> does not exist: Card is composed from flat parts. Use <CardTitle> (import { CardTitle } from "@/components/ui/card").
no-unknown-prop
error by defaultProps must exist on the component (own props or the HTML attributes it forwards).
- Catches
- Props a component does not accept, own or inherited (
tone,isDisabled) - Suggests
- Closest prop, with cross-library synonyms (
tone→variant) and equivalents in either direction (checked↔isSelected,open↔isOpen);asChildon a Base UI component (orrenderon a Radix one) gets how that library composes - Why it matters
- Props from other libraries (
tone,isDisabled,isOpen) do nothing or break the build. The rule suggests the prop this system uses, with its allowed values, and explains how Radix (asChild) and Base UI (render) compose when an agent mixes them up.
<div className="grid gap-2"> <Badge tone="success">Active</Badge> <Input isDisabled placeholder="Workspace name" /></div><div className="grid gap-2"> <Badge variant="success">Active</Badge> <Input disabled placeholder="Workspace name" /></div>error2:10fix included
<Badge> has no prop "tone". Did you mean "variant" ("default" | "secondary" | "destructive" | "success" | "outline")?
error3:10fix included
<Input> has no prop "isDisabled". Did you mean "disabled"?
no-unknown-variant
error by defaultVariant props (cva variants and string-literal unions) only accept their declared values.
- Catches
- Values outside a cva variant or literal union (
variant="danger") - Suggests
- Allowed values and a synonym match (
danger→destructive,small→sm) - Why it matters
variant="danger"on aButtonthat only knowsdestructiverenders without that variant's styles, so the mistake looks like a working button. The rule lists the allowed values and maps synonyms across libraries (danger→destructive,small→sm).
<Button variant="danger" size="small"> Delete workspace</Button><Button variant="destructive" size="sm"> Delete workspace</Button>error1:17fix included
"danger" is not a valid variant for <Button>. Allowed: default, destructive, outline, secondary, ghost, link. Did you mean "destructive"?
error1:31fix included
"small" is not a valid size for <Button>. Allowed: default, sm, lg, icon. Did you mean "sm"?
Configuring the rules
Set a rule to "off", "warn" or "error", or give it values to accept with [severity, { "allow": [...] }], in design-system-mcp.config.json. Rules that need tokens skip themselves when the design system has none of that kind, and code that does not parse is reported under syntax. The config file covers the rest.
{ "rules": { "no-hardcoded-spacing": "error", "no-hardcoded-color": ["error", { "allow": ["#fff"] }], "icon-button-accessible-name": "off" }}How fixes are chosen
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, or a gray for a gray, so a pale yellow is never swapped for a light gray that happens to be close. 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 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.