Design Token Lint
GitHub repository

Type to search...

to open search from anywhere

You are viewing documentation for an older version.View the latest version

Examples

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-4 … p-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"],
  "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 is left alone by this config — a text-{n} pattern only matches numeric values, so it can never match a non-numeric size keyword like xs; catching text-xs would require an exact-match entry such as "text-xs" in 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 intent — bg-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 drift — p-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"],
  "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 '@takazudo/zudo-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.