Design Token Lint

Type to search...

to open search from anywhere

Examples

CreatedApr 10, 2026UpdatedApr 10, 2026Takeshi Takatsudo

Concrete examples of design token enforcement using the tight token strategy.

Real-world configurations and usage patterns — with concrete tokens, not generic placeholders.

The Problem This Tool Solves

Tailwind’s default spacing scale has 30+ numeric steps (p-1, p-2, p-3, p-4p-96). When any value is valid, every developer picks something slightly different:

// Developer A's component — uses p-4
<div className="p-4 bg-gray-200">

// Developer B's component — uses p-5 for "medium padding"
<div className="p-5 bg-gray-100">

// Developer C's component — uses p-6
<div className="p-6 bg-gray-300">

All three compile fine. The UI slowly drifts apart.

design-token-lint flags these numeric utilities so only project-defined semantic tokens are used. One lint rule, uniform spacing everywhere.

A Concrete Token System

The examples on this page use a real token system — the same one this documentation site is built with.

Spacing tokens

Two semantic axes replace the single numeric scale:

Horizontal spacing (hsp-*) — inline gaps, horizontal padding:

TokenValueUse
hsp-2xs2pxtight inline spacing
hsp-xs6pxcompact inline
hsp-sm8pxsmall padding
hsp-md12pxdefault gaps
hsp-lg16pxstandard padding
hsp-xl24pxgenerous padding
hsp-2xl32pxlarge padding

Vertical spacing (vsp-*) — section gaps, vertical padding:

TokenValueUse
vsp-2xs7pxtight gap
vsp-xs14pxsmall gap
vsp-sm20pxcompact gap
vsp-md24pxstandard gap
vsp-lg28pxsection gap
vsp-xl40pxlarge section gap
vsp-2xl56pxpage-level gap

Color tokens

Semantic names instead of palette shades:

TokenTailwind classMeaning
surfacebg-surfacecard/sidebar backgrounds
mutedtext-mutedsecondary, de-emphasized text
accentbg-accentprimary interactive color
accent-hoverbg-accent-hoverhover state for accent
code-bgbg-code-bginline code background
code-fgtext-code-fginline code text
successtext-successpositive states
dangertext-dangererror / destructive states
warningtext-warningcaution states
infotext-infoinformational states

These are registered in @theme in your global CSS — there is no bg-gray-200 or bg-blue-600 because those tokens do not exist. Attempting to use them is a lint violation.

What the Linter Catches

Config for this token system:

{
  "prohibited": [
    "p-{n}", "px-{n}", "py-{n}", "pt-{n}", "pr-{n}", "pb-{n}", "pl-{n}",
    "m-{n}", "mx-{n}", "my-{n}", "mt-{n}", "mr-{n}", "mb-{n}", "ml-{n}",
    "gap-{n}", "gap-x-{n}", "gap-y-{n}",
    "space-x-{n}", "space-y-{n}",
    "bg-{color}-{shade}",
    "text-{color}-{shade}",
    "border-{color}-{shade}"
  ],
  "allowed": ["p-0", "m-0", "gap-0", "p-px"],
  "patterns": ["src/**/*.{tsx,jsx,astro}"],
  "ignore": ["**/*.test.*", "**/*.stories.*"],
  "suggestionSuffix": "use semantic token (hsp-*/vsp-*) or arbitrary value"
}

Violations the linter reports:

ClassReasonFix
p-4matches p-{n}p-hsp-lg (16px standard padding)
gap-2matches gap-{n}gap-hsp-sm (8px small gap)
bg-gray-200matches bg-{color}-{shade}bg-surface (surface background)
text-gray-500matches text-{color}-{shade}text-muted (secondary text)
bg-blue-600matches bg-{color}-{shade}bg-accent (primary color)

Before and After

A card component using raw Tailwind, then the same component with semantic tokens.

Before — raw numeric utilities

// ArticleCard.tsx — flagged by design-token-lint
export function ArticleCard({ title, excerpt, tag }: Props) {
  return (
    <div className="p-4 bg-gray-100 rounded-lg">
      <h2 className="text-lg font-bold text-gray-900 mb-2">{title}</h2>
      <p className="text-sm text-gray-600 mb-4">{excerpt}</p>
      <div className="flex gap-2">
        <span className="px-3 py-1 bg-blue-600 text-white text-xs rounded">
          {tag}
        </span>
      </div>
    </div>
  )
}

The linter flags: p-4, bg-gray-100, text-gray-900, mb-2, text-gray-600, mb-4, gap-2, px-3, py-1, bg-blue-600, text-xs (if numeric text sizes are prohibited).

After — semantic tokens

// ArticleCard.tsx — clean
export function ArticleCard({ title, excerpt, tag }: Props) {
  return (
    <div className="p-hsp-lg bg-surface rounded-lg">
      <h2 className="text-subheading font-bold text-fg mb-vsp-2xs">{title}</h2>
      <p className="text-small text-muted mb-vsp-xs">{excerpt}</p>
      <div className="flex gap-hsp-sm">
        <span className="px-hsp-sm py-vsp-2xs bg-accent text-bg text-caption rounded">
          {tag}
        </span>
      </div>
    </div>
  )
}

Why the semantic version is better

Centralized changes — when the design team adjusts the accent color, updating one CSS variable (--color-accent) updates every bg-accent usage across the entire codebase. With bg-blue-600, you have to grep and replace across hundreds of files.

Readable intentbg-surface tells you “this is a surface-level background”. bg-gray-100 tells you nothing except a number. Six months later, the token name still makes sense.

No driftp-hsp-lg is the same value everywhere. p-4 is only 16px if nobody changed the Tailwind config. Semantic tokens survive theme overrides.

Enforceable — the linter turns a style-guide footnote into a CI failure. Violations are caught at PR time, not in design review.

Realistic Config Examples

A project where Tailwind defaults are fully replaced by semantic tokens:

{
  "prohibited": [
    "p-{n}", "px-{n}", "py-{n}", "pt-{n}", "pr-{n}", "pb-{n}", "pl-{n}",
    "m-{n}", "mx-{n}", "my-{n}", "mt-{n}", "mr-{n}", "mb-{n}", "ml-{n}",
    "gap-{n}", "gap-x-{n}", "gap-y-{n}",
    "space-x-{n}", "space-y-{n}",
    "bg-{color}-{shade}",
    "text-{color}-{shade}",
    "border-{color}-{shade}",
    "ring-{color}-{shade}"
  ],
  "allowed": ["p-0", "m-0", "gap-0", "p-px"],
  "ignore": [
    "**/*.test.*",
    "**/*.stories.*",
    "**/vendor/**"
  ],
  "patterns": [
    "src/**/*.{tsx,jsx,astro}",
    "components/**/*.{tsx,jsx,astro}"
  ],
  "suggestionSuffix": "use semantic token (hsp-*/vsp-*) or arbitrary value"
}

Gradual migration

Already have a large codebase with raw Tailwind? Start with colors only, then add spacing:

{
  "prohibited": [
    "bg-{color}-{shade}",
    "text-{color}-{shade}",
    "border-{color}-{shade}"
  ],
  "allowed": [],
  "patterns": ["src/**/*.{tsx,jsx}"],
  "suggestionSuffix": "use a semantic color token instead"
}

Once colors are clean, add spacing patterns to prohibited.

Next.js project

{
  "prohibited": [
    "p-{n}", "m-{n}", "gap-{n}",
    "bg-{color}-{shade}",
    "text-{color}-{shade}"
  ],
  "allowed": ["p-0", "m-0", "gap-0"],
  "ignore": [
    "**/*.test.*",
    "**/*.stories.*",
    ".next/**"
  ],
  "patterns": [
    "app/**/*.{tsx,jsx}",
    "components/**/*.{tsx,jsx}",
    "pages/**/*.{tsx,jsx}"
  ]
}

Astro project

{
  "prohibited": [
    "p-{n}", "m-{n}", "gap-{n}",
    "bg-{color}-{shade}",
    "text-{color}-{shade}",
    "border-{color}-{shade}"
  ],
  "allowed": ["p-0", "m-0", "gap-0"],
  "ignore": [
    "**/*.test.*",
    "dist/**",
    ".astro/**"
  ],
  "patterns": [
    "src/**/*.{astro,tsx,jsx}"
  ]
}

Monorepo with Multiple Packages

Place a config at the monorepo root and scan all packages:

{
  "prohibited": [
    "p-{n}", "m-{n}", "gap-{n}",
    "bg-{color}-{shade}",
    "text-{color}-{shade}"
  ],
  "allowed": ["p-0", "m-0", "gap-0"],
  "ignore": [
    "**/node_modules/**",
    "**/dist/**",
    "**/*.test.*"
  ],
  "patterns": [
    "packages/*/src/**/*.{tsx,jsx,astro}",
    "apps/*/src/**/*.{tsx,jsx,astro}"
  ]
}

CI Integration — GitHub Actions

Fail CI on violations:

# .github/workflows/lint.yml
name: Lint
on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm design-token-lint

Pre-push Hook

Run on every push with lefthook:

# lefthook.yml
pre-push:
  commands:
    design-token-lint:
      run: npx design-token-lint

Custom Script

Use the programmatic API to run the linter with project-specific logic:

// scripts/lint-tokens.mjs
import { loadConfig, compileConfig, setConfig, lintFile } from '@zudolab/design-token-lint';
import { glob } from 'glob';

const config = await loadConfig(process.cwd());
setConfig(compileConfig(config));

const files = await glob('src/**/*.{tsx,jsx}');
let totalViolations = 0;

for (const file of files) {
  const results = await lintFile(file);
  for (const r of results) {
    console.log(`${r.filePath}:${r.line}  ${r.className}  ${r.reason}`);
    totalViolations++;
  }
}

process.exit(totalViolations > 0 ? 1 : 0);

Ignoring Legitimate Exceptions

When integrating a third-party component that requires raw Tailwind:

{/* design-token-lint-ignore — vendor component requires literal p-4 */}
<VendorCalendar className="p-4 bg-gray-50" />

See ignore syntax for all comment forms.

Revision History