Rules Reference
Rules run during dna check --only=lint. Each rule targets a specific class of design drift.
Severities are configuration, not constants. Each rule's severity comes from your
ds-lint.config.json("error","warn", or"off"). The severities shown below are common defaults — check your own config for what actually gates.
color-family-allowlist
Severity: error
Flags raw color literals that fall inside the palette hue ranges but aren't expressed as design tokens. The "allowlist" is your palette — any raw value close enough to a palette hue is considered a drift violation.
What it catches:
- Raw hex values:
#4F84D6,#bf8a40 - Raw rgb/rgba:
rgba(182, 141, 66, 0.1),rgb(79, 132, 214) - Raw hsl/hsla:
hsl(38 47% 48%)
Does not flag:
- Values in
:rootwhere tokens are defined transparent,currentColor,inherit- Colors in the
excludePathsconfig
Example finding:
color-family-allowlist error
src/components/Badge.module.css:22
rgba(182, 141, 66, 0.1)
hint: use var(--brand)
Fix: Replace the literal with the appropriate CSS custom property.
/* before */
background: rgba(182, 141, 66, 0.1);
/* after */
background: color-mix(in srgb, var(--brand) 10%, transparent);
off-grid-spacing
Severity: error
Flags spacing values that don't land on the extracted grid unit. If your grid is 4px, any margin, padding, gap, or size that isn't a multiple of 4 is an error.
What it catches:
padding: 6pxon a 4px gridmargin-top: 10pxon an 8px grid- Tailwind classes like
p-[13px]orgap-[6px]
Does not flag:
- Values of
0 1pxborder-like values (configurable)- Values in
excludePaths
Example finding:
off-grid-spacing error
src/components/Card.module.css:34
padding: 6px
hint: nearest grid values are 4px, 8px
Fix: Round to the nearest grid multiple.
/* before — 4px grid */
padding: 6px 10px;
/* after */
padding: 8px 12px;
Modes. By default the rule checks grid divisibility, not scale membership: at grid: 4, p-11 (44px) passes because 44 % 4 === 0, even if 44 was never a declared step. Opt into strict membership with mode: "scale":
"off-grid-spacing": { "severity": "error", "mode": "scale", "scale": [4, 8, 12, 16, 24, 32] }
In scale mode a value must be an exact member of the declared array. The array resolves from (most specific wins): the rule config's own scale, else the identity's scales.spacingScale (written by dna start when your tailwind.config.* declares a real theme.spacing). If mode: "scale" is set but neither source resolves, the rule falls back to the grid check rather than silently going permissive.
arbitrary-values
Severity: warning
Flags Tailwind arbitrary-value syntax (p-[13px], text-[#abc]) that bypasses the design scale. Arbitrary values are a design escape hatch — fine in prototypes, but each one is a point where design tokens can't enforce constraints.
What it catches:
class="p-[13px]",class="w-[342px]",class="text-[#4F84D6]"- Combined classes like
class="flex gap-[6px] items-center"
Fix: Use a configured Tailwind scale value, or add the value to the scale.
/* before */
<div className="p-[13px]">
/* after */
<div className="p-3"> {/* 12px, nearest grid step */}
prefer-semantic
Severity: warning
Flags direct use of primitive tokens where a semantic alias exists. If your identity has both --yellow-500 and --warning, using --yellow-500 in a component context is a drift signal.
Example finding:
prefer-semantic warning
src/components/Alert.tsx:14
var(--yellow-500)
hint: prefer var(--warning)
prefer-token
Severity: warning
Flags font-size, font-weight, or line-height literals that match extracted type-scale values. This is the type-scale equivalent of color-family-allowlist.
What it catches:
font-size: 14pxwhen14is in the type scale as--text-bodyfont-size: 34pxwhen34is--text-display-xl
Fix:
/* before */
font-size: 14px;
/* after */
font-size: var(--text-body);
prefer-token-class
Severity: warning (typical)
The Tailwind counterpart to prefer-token: flags an arbitrary color class (bg-[#2E7D32], text-[#hex]) whose value matches an identity token that already has a token class. Instead of only saying "arbitrary value", the finding names the class to use.
/* before */
<div className="bg-[#2E7D32]">
/* after */
<div className="bg-primary">
Works with shadcn/ui HSL-channel tokens too — a hex that resolves to a corroborated --primary downgrades to this rule naming the token.
prefer-scale-step
Severity: warning (typical)
Flags a spacing/size value that is on-grid but should be one of the declared scale steps — the advisory companion to off-grid-spacing's mode: "scale". Where a declared scale exists (scales.spacingScale in the identity, or the rule config), values between steps are nudged toward the nearest real step.
typography-role
Severity: warning
Flags text that uses a display-size token in a body context, or a body-size token in a display context. DNA infers which scale values are "display" vs "body" by their relative magnitude and where they appear in the codebase.
What it catches:
- A paragraph element using
var(--text-display-xl) - A heading using
var(--text-xs)with no size override
ghost-tokens
Severity: warning
Flags var() references to tokens that don't exist in the extracted identity. Ghost tokens are commonly left behind after a palette rename, or used as aspirational placeholders before the token is defined.
Example finding:
ghost-tokens warning
src/components/Chip.module.css:8
var(--accent-muted)
hint: no such token in .dna/identity.yaml
Fix: Either add --accent-muted to your :root token definitions, or change the reference to an existing token.
A token used only via
hsl(var(--x))composition (the shadcn convention) counts as referenced — it is never falsely reported as unused.
cn-merge guard
Severity: error
Layer: cn-merge (not lint — runs as a separate check pass)
Flags cn() / clsx() / twMerge() calls where a class argument contains a raw color or spacing literal that bypasses the token system. This catches drift that escapes the source-file lint pass because it's assembled at call-site.
What it catches:
cn("flex", "bg-[#4F84D6]") // raw hex in cn call
cn(base, isActive && "p-[7px]") // off-grid arbitrary in conditional
Fix: Use token-based classes inside cn().
cn("flex", "bg-[var(--brand)]")
cn(base, isActive && "p-2")
Configuring rules
Rules are configured in ds-lint.config.json. To disable a rule or change its severity:
{
"rules": {
"arbitrary-values": "off",
"ghost-tokens": "warn",
"prefer-semantic": "off"
},
"excludePaths": [
"src/styles/legacy/**"
]
}
Valid severity values: "error", "warn", "off".
Errors cause
dna checkto exit 1 and block the pre-commit hook. Warnings are advisory — they print but do not block.