# Design Token Lint > Lint Tailwind CSS class names against design system tokens. Enforce semantic spacing and color tokens instead of raw numeric utilities. --- # Changelog > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/changelog --- # Guide > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/guide --- # Overview > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/overview --- # Reference > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/reference --- # Configuration > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/guide/configuration Create a `.design-token-lint.json` or `design-token-lint.config.json` file at your project root. The linter loads the first file it finds, falling back to built-in defaults if neither exists. ## Full Example ```json { "prohibited": [ "p-{n}", "px-{n}", "py-{n}", "m-{n}", "gap-{n}", "bg-{color}-{shade}", "text-{color}-{shade}", "border-{color}-{shade}" ], "allowed": ["p-0", "m-0", "gap-0", "p-1px"], "ignore": ["**/*.test.*", "**/*.stories.*"], "patterns": [ "src/**/*.{tsx,jsx,astro}", "components/**/*.{tsx,jsx,astro}" ] } ``` ## Fields | Field | Type | Description | |---|---|---| | `prohibited` | `string[]` | Patterns to flag as violations | | `allowed` | `string[]` | Exceptions that always pass, even if they match a prohibited pattern | | `ignore` | `string[]` | File glob patterns to skip entirely | | `patterns` | `string[]` | File glob patterns to scan (used when no CLI args are given) | | `suggestionSuffix` | `string` | Custom suffix for violation messages (replaces the default suggestion text) | All fields are optional and fall back to built-in defaults. ### `prohibited` An array of class name patterns to flag. Each pattern uses a placeholder syntax: - **`{n}`** — matches numeric values like `4`, `8`, `0.5`, `16`. Used for spacing (padding, margin, gap, inset, top/left/right/bottom, etc.) - **`{color}`** — matches standard Tailwind color names: `slate`, `gray`, `zinc`, `neutral`, `stone`, `red`, `orange`, `amber`, `yellow`, `lime`, `green`, `emerald`, `teal`, `cyan`, `sky`, `blue`, `indigo`, `violet`, `purple`, `fuchsia`, `pink`, `rose` - **`{shade}`** — matches 2-3 digit shade values like `50`, `100`, `500`, `950` Examples: - `p-{n}` matches `p-4`, `p-8`, `p-0.5` - `bg-{color}-{shade}` matches `bg-red-500`, `bg-blue-300` - `gap-x-{n}` matches `gap-x-2`, `gap-x-6` ### `allowed` An exact-match allowlist. Any class in this array always passes, regardless of prohibited patterns. Useful for escape hatches like `p-0`, `m-0`, or `p-1px`. ### `ignore` File glob patterns to skip entirely. Common patterns: ```json { "ignore": [ "**/*.test.*", "**/*.stories.*", "**/*.spec.*" ] } ``` ### `patterns` File glob patterns to scan when the CLI is called without explicit file arguments. If omitted, the CLI uses a default set (`src/**`, `components/**`, `lib/**`, `app/**`). ### `suggestionSuffix` A string appended to violation messages after the `—` separator, replacing the default suggestion text. Use this to point developers toward your project's specific token naming convention. **Default messages (no `suggestionSuffix`):** - Spacing: `Numeric spacing "p-4" — use a semantic spacing token or arbitrary value` - Color: `Default Tailwind color "bg-gray-500" — use a design system color token` **With `suggestionSuffix`:** ```json { "suggestionSuffix": "use hgap-*/vgap-* or zd-* tokens" } ``` - Spacing: `Numeric spacing "p-4" — use hgap-*/vgap-* or zd-* tokens` - Color: `Default Tailwind color "bg-gray-500" — use hgap-*/vgap-* or zd-* tokens` ## Built-in Defaults If no config file exists, the linter uses these defaults: - **Prohibited**: all standard spacing utilities (`p-*`, `m-*`, `gap-*`, `inset-*`, `scroll-*`) with numeric values, plus all color utilities (`bg-*`, `text-*`, `border-*`, `ring-*`, etc.) with default Tailwind color-shade combinations - **Allowed**: `p-0`, `m-0`, `gap-0`, `p-1px` - **Ignore**: `**/*.test.*`, `**/*.stories.*` See the [package README](https://github.com/Takazudo/zudo-design-token-lint/blob/main/README.md) for the full default list. ## What Passes Automatically These classes always pass without being in `allowed`: - **Semantic spacing tokens**: `p-hgap-sm`, `gap-vgap-xs`, `m-hgap-md` (classes with `hgap-*` or `vgap-*` suffixes) - **Non-default colors**: `bg-surface`, `text-fg`, `bg-zd-black` (any color name that isn't one of the standard Tailwind palette names) - **Arbitrary values**: `w-[28px]`, `bg-[#123]`, `p-[10px]` - **Non-spacing and non-color utilities**: `flex`, `grid`, `hidden`, `w-full`, `font-bold`, etc. --- # What is design-token-lint? > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/overview/what-is Tailwind CSS provides hundreds of numeric utilities — `p-1` through `p-96`, `gap-2`, `mt-8` — and a default color palette with shades like `bg-red-500`, `text-gray-300`. These utilities are convenient, but they let developers bypass any design system entirely. Anyone can pick an arbitrary spacing value or color without referencing design tokens. `@zudolab/design-token-lint` is a linter that catches this. It scans source files for Tailwind class names that match prohibited patterns and reports them as violations — pointing developers toward semantic alternatives. ## The Problem A component with `p-4` and another with `p-6` may look similar, but they carry no shared semantic meaning. Multiply this across a large project: dozens of one-off spacing values, colors picked from the Tailwind palette rather than design tokens, and no tooling to enforce consistency. Design systems define semantic tokens — `spacing-md`, `color-surface`, `text-primary` — to give these values a contract. Raw utilities bypass that contract silently. See the [methodology page](../../reference/methodology/index.md) for the full reasoning. ## How It Works 1. **Scan files** — the linter reads source files (`.tsx`, `.jsx`, `.astro`, `.vue`, `.html`, etc.) and extracts Tailwind class names using pattern matching against `className`, `class`, `cn()`, `clsx()`, and similar syntaxes. 2. **Match against rules** — each extracted class is checked against prohibited patterns defined in `.design-token-lint.json`. Patterns use placeholders: - `{n}` — matches any Tailwind numeric step (`p-{n}` matches `p-4`, `p-6`, `p-12`, ...) - `{color}` — matches any default Tailwind color name (`bg-{color}-{shade}` matches `bg-red-500`) - `{shade}` — matches numeric shades (`50`–`950`) 3. **Report violations** — violations are reported with file path, line number, class name, and a reason string that suggests the expected token category. ``` src/Button.tsx L12: p-4 — Numeric spacing "p-4" — use semantic token (hgap-*/vgap-*) or arbitrary value src/Card.tsx L7: bg-gray-200 — Default color "bg-gray-200" — use semantic token (bg-surface/bg-muted/...) ``` ## Key Features - **CLI tool** — run `npx design-token-lint` to lint a project; exits with code `1` on violations for CI integration - **Configurable rules** — define prohibited patterns and an explicit allowlist in `.design-token-lint.json` - **Pattern placeholders** — `{n}`, `{color}`, `{shade}` let one rule cover entire families of utilities - **Ignore syntax** — suppress individual lines with `// design-token-lint-ignore` or entire files with `// design-token-lint-ignore-file` - **Programmatic API** — `lintFile()`, `lintContent()`, `checkClass()` for integration with build tools and editors - **Browser playground** — try patterns and classes interactively without installing anything ## Tech Stack TypeScript, Node.js 18+. Distributed as an npm package with a CLI binary and an importable library. Runtime dependencies: `chalk` (terminal colors) and `glob` (file scanning). No Tailwind dependency — it works by string pattern matching, not Tailwind's internals. ## Integration Works with any framework that uses Tailwind class names in source files — React, Vue, Astro, Svelte, or plain HTML. Integrate with: - **CI/CD** — run as a step in GitHub Actions or any pipeline - **Git hooks** — run before push with [lefthook](https://github.com/evilmartians/lefthook) or husky - **Build tools** — use the programmatic API in Vite plugins, webpack loaders, or custom scripts - **Editors** — the programmatic API supports editor plugin authors --- # Playground > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/playground Try the linter directly in your browser. Paste your code on the left, adjust the configuration, and see violations on the right. --- # Programmatic API > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/reference/api `@zudolab/design-token-lint` exports a small API for integration with build tools, editors, or custom tooling. ## Installation ```bash pnpm add @zudolab/design-token-lint ``` ## Exports ```ts // File/content linting lintFile, lintContent, type LintResult, // Single-class checking checkClass, checkClassWithConfig, type Violation, // Config loading and compilation loadConfig, compileConfig, compilePattern, setConfig, getConfig, DEFAULT_CONFIG, type LintConfig, type CompiledConfig, type CompiledRule, // Class extraction extractClasses, type ExtractedClass, } from '@zudolab/design-token-lint'; ``` ## Linting Files and Content ### `lintFile(filePath)` Read a file from disk and return an array of lint results — one entry per violation. ```ts const results = await lintFile('src/App.tsx'); for (const r of results) { console.log(`${r.filePath}:${r.line} ${r.className} ${r.reason}`); } ``` Returns `Promise`: ```ts interface LintResult { filePath: string; line: number; className: string; reason: string; } ``` Each entry is a flat record for one violation. If the file has no violations, the returned array is empty. ### `lintContent(filePath, content)` Lint a string directly — useful for editor plugins or in-memory content. Returns `LintResult[]` (same shape as above). ```ts const results = lintContent('file.tsx', ''); // [ // { filePath: 'file.tsx', line: 1, className: 'p-4', reason: '...' }, // { filePath: 'file.tsx', line: 1, className: 'bg-gray-500', reason: '...' } // ] ``` ## Checking a Single Class ### `checkClass(className)` Check one class name against the active config. Returns a `Violation` if the class is prohibited, or `null` if it passes. ```ts const violation = checkClass('p-4'); if (violation) { console.error(violation.reason); // "Numeric spacing \"p-4\" — use semantic token (hgap-*/vgap-*) or arbitrary value" } ``` Returns `Violation | null`: ```ts interface Violation { className: string; reason: string; } ``` ### `checkClassWithConfig(className, compiledConfig)` Same as above, but with an explicit compiled config instead of the global one. ```ts const config = await loadConfig(process.cwd()); const compiled = compileConfig(config); const violation = checkClassWithConfig('bg-blue-500', compiled); ``` ## Working with Config ### `loadConfig(cwd)` Load `.design-token-lint.json` or `design-token-lint.config.json` from a directory. Falls back to `DEFAULT_CONFIG` if neither exists. ```ts const config = await loadConfig(process.cwd()); ``` ### `compileConfig(config)` Compile a plain config object into an efficient rule set ready for matching. ```ts const compiled = compileConfig({ prohibited: ['p-{n}', 'bg-{color}-{shade}'], allowed: ['p-0'], ignore: [], }); ``` ### `setConfig(compiled)` / `getConfig()` Set or get the global compiled config used by `checkClass()` and `lintFile()`. ```ts setConfig(compiled); const active = getConfig(); ``` ### `compilePattern(pattern)` Compile a single pattern string (like `p-{n}`) into a `CompiledRule`. ```ts const rule = compilePattern('bg-{color}-{shade}'); // { prefix: 'bg', valuePattern: /^(slate|gray|...)-(\d{2,3})$/, reasonTemplate: '...', isSpacingRule: false } ``` ## Extracting Classes ### `extractClasses(content)` Extract all class name tokens from a source file string, with their line numbers. ```ts const extracted = extractClasses(''); // [ // { className: 'p-4', line: 1 }, // { className: 'bg-red-500', line: 1 } // ] ``` Returns `ExtractedClass[]`: ```ts interface ExtractedClass { className: string; line: number; } ``` Supported syntaxes: - `className="..."` and `class="..."` (JSX/Astro) - `className={'...'}` single-quote brace - `` className={`...`} `` template literals (simple cases) - `class:list={["...", '...']}` Astro class:list arrays - `cn(...)`, `clsx(...)`, `classNames(...)`, `twMerge(...)` utility calls ## Types ### `LintConfig` ```ts interface LintConfig { prohibited: string[]; allowed: string[]; ignore: string[]; patterns?: string[]; suggestionSuffix?: string; } ``` ### `LintResult` ```ts interface LintResult { filePath: string; line: number; className: string; reason: string; } ``` ### `Violation` ```ts interface Violation { className: string; reason: string; } ``` ## Example: Custom Linter Script ```ts loadConfig, compileConfig, setConfig, lintFile, } from '@zudolab/design-token-lint'; async function main() { 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); } main(); ``` --- # /CLAUDE.md > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/claude-md/root **Path:** `CLAUDE.md` # CLAUDE.md ## Project `@zudolab/design-token-lint` — a linter that enforces semantic design tokens instead of raw Tailwind numeric utilities. - **Root**: The npm package (TypeScript + vitest) - **`doc/`**: Astro-based documentation site (zudo-doc) deployed at `/pj/zudo-design-token-lint/` ## Directory Layout ``` zudo-design-token-lint/ ├── src/ # Lint package source (TypeScript) │ ├── cli.ts # CLI entry point (#!/usr/bin/env node) │ ├── config.ts # Config loading and pattern compilation │ ├── extractor.ts # Class name extraction from source files │ ├── rules.ts # Rule matching against compiled config │ ├── linter.ts # Main linter combining extraction + rules │ ├── index.ts # Public API exports │ └── *.test.ts # Tests (colocated) ├── dist/ # Build output ├── package.json # Lint package manifest (primary) ├── tsconfig.json # Lint package TS config ├── vitest.config.ts # Vitest config ├── .design-token-lint.json # Dogfooding config ├── .prettierrc # Prettier config ├── README.md # Lint package README ├── LICENSE ├── doc/ # Astro doc site │ ├── src/ # Astro source │ ├── astro.config.ts # Astro config │ ├── tsconfig.json # Astro TS config │ └── package.json # Astro site package.json ├── pnpm-workspace.yaml # pnpm workspace: ["doc"] └── .github/workflows/ # CI + publish workflows ``` ## Commands (Root — Lint Package) ```bash pnpm build # Compile TypeScript to dist/ (tsc) pnpm test # Run tests (vitest run) pnpm test:watch # Watch mode pnpm lint # prettier --check . pnpm lint:fix # prettier --write . ``` ## Commands (Doc Site — Workspace Shortcuts) ```bash pnpm dev:doc # Start Astro dev server pnpm build:doc # Build doc site to doc/dist/ pnpm preview:doc # Preview built doc site pnpm check:doc # Astro type check ``` ## API Shapes (Important) - `LintResult` is **flat**: `{ filePath, line, className, reason }` — NOT `{ filePath, violations: [...] }` - `lintFile()` and `lintContent()` return `LintResult[]` (array, not single object) - `Violation` has only `{ className, reason }` — no `line` or `column` - `checkClass()` returns `Violation | null` — not `undefined` - `ExtractedClass` has `{ className, line }` — no `column` Keep the public documentation (`doc/src/content/docs/api/`) in sync when changing these shapes. ## Deployment The doc site deploys to `/pj/zudo-design-token-lint/` on Cloudflare Pages. `settings.base` in `doc/src/config/settings.ts` must match. - **Production**: Push to `main` triggers `.github/workflows/doc-deploy.yml` → deploys to Cloudflare Pages (`main` branch) - **PR Preview**: PRs targeting `main` trigger `.github/workflows/doc-preview.yml` → deploys to `pr-.zudo-design-token-lint.pages.dev` Deploy directory structure: `deploy/pj/zudo-design-token-lint/` with a `_redirects` file routing `/` → `/pj/zudo-design-token-lint/`. Required secrets: `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`. ## CI / Publish - `.github/workflows/ci.yml` — test + build + lint on PR and push to main - `.github/workflows/doc-deploy.yml` — deploy doc site to Cloudflare Pages on push to main (requires `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`) - `.github/workflows/doc-preview.yml` — deploy doc site preview on PRs, posts preview URL as PR comment (requires `CLOUDFLARE_API_TOKEN`, `CLOUDFLARE_ACCOUNT_ID`) - `.github/workflows/publish.yml` — publish to npm when a `v*.*.*` tag is pushed (requires `NPM_TOKEN` secret) ## Publishing Triggered by pushing a `v*.*.*` tag to main. The `.github/workflows/publish.yml` workflow runs tests + build + `pnpm publish --access public`. Requires `NPM_TOKEN` secret. ## Dogfooding `.design-token-lint.json` at root configures the linter on its own source code. Run `pnpm dlx @zudolab/design-token-lint` (after publish) or `node dist/cli.js` to lint. ## Commit Messages Use conventional format: `feat:`, `fix:`, `docs:`, `chore:`, `test:`, `refactor:`, `ci:` ## Subdirectory Rules - **Writing or editing documentation?** Read `doc/src/content/CLAUDE.md` --- # CLI > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/guide/cli The `design-token-lint` CLI scans files for prohibited Tailwind class names and reports violations. ## Basic Usage ```bash # Scan default patterns (from config.patterns or built-in defaults) design-token-lint # Scan specific files or globs design-token-lint "src/**/*.tsx" "pages/**/*.tsx" # Scan a single file design-token-lint src/App.tsx ``` ## Exit Codes | Code | Meaning | |---|---| | `0` | No violations found (or no files matched) | | `1` | Violations found | | `2` | Unexpected error (e.g., file system failure) | Use exit code `1` in CI to fail builds when violations appear: ```yaml # .github/workflows/lint.yml - run: pnpm design-token-lint ``` ## Output Format Violations are grouped by file. Each line shows the line number, the offending class, and the reason: ``` Scanning 1 file(s)... src/App.tsx L12: p-4 — Numeric spacing "p-4" — use semantic token (hgap-*/vgap-*) or arbitrary value L12: bg-gray-500 — Default Tailwind color "bg-gray-500" — use design system token (zd-*, p0-p15, semantic) Found 2 violation(s) in 1 file(s). ``` All output goes to stderr so it doesn't interfere with scripts that pipe stdout. ## File Pattern Resolution When called with no arguments, the CLI resolves files in this order: 1. If `config.patterns` is set, use those globs 2. Otherwise, use the built-in defaults: `src/**/*.{tsx,jsx,astro}`, `components/**/*.{tsx,jsx,astro}`, `lib/**/*.{tsx,jsx}`, `app/**/*.{tsx,jsx}` When called with arguments, each argument is treated as a file path or glob. The `ignore` config still applies. ## Integration with package.json Add a script for convenient invocation: ```json { "scripts": { "lint:tokens": "design-token-lint" } } ``` Then run: ```bash pnpm lint:tokens ``` ## Integration with lefthook Run on push with [lefthook](https://github.com/evilmartians/lefthook): ```yaml # lefthook.yml pre-push: commands: design-token-lint: run: npx design-token-lint ``` ## Integration with CI Add a GitHub Actions step: ```yaml - name: Lint design tokens run: pnpm design-token-lint ``` Any violation will exit with code `1` and fail the workflow. --- # Getting Started > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/overview/getting-started `@zudolab/design-token-lint` lints Tailwind CSS class names against design system tokens. It catches raw numeric utilities like `p-4`, `gap-6`, and default palette colors like `bg-gray-500`, and guides developers toward semantic tokens. ## Why Tailwind's numeric utilities and default color palette make it easy to introduce inconsistency across a codebase. A developer writes `p-4` in one place and `p-6` in another — visually similar, semantically unrelated. Multiply this across a large codebase and design consistency breaks down. This linter enforces a simple rule: **raw numbers and default palette colors are not allowed**. Instead, use semantic tokens that describe intent: `p-hgap-md` for horizontal spacing, `bg-surface` for surfaces, `text-fg` for primary text. See the [methodology page](../../reference/methodology/index.md) for the full reasoning. ## Installation Install as a dev dependency in your Tailwind project: ```bash pnpm add -D @zudolab/design-token-lint ``` Or with npm / yarn: ```bash npm install --save-dev @zudolab/design-token-lint yarn add --dev @zudolab/design-token-lint ``` ## First Run Run the linter with no arguments to scan the default file patterns (`src/`, `components/`, `lib/`, `app/`): ```bash npx design-token-lint ``` The CLI exits with code `0` if no violations are found, or `1` if there are violations — suitable for use in CI. ## Your First Config Create a `.design-token-lint.json` file at your project root to customize rules: ```json { "prohibited": [ "p-{n}", "m-{n}", "gap-{n}", "bg-{color}-{shade}", "text-{color}-{shade}" ], "allowed": ["p-0", "m-0", "gap-0"], "ignore": ["**/*.test.*", "**/*.stories.*"] } ``` See the [configuration reference](../../guide/configuration/index.md) for the full list of options. ## Next Steps - Read the [configuration reference](../../guide/configuration/index.md) to tailor rules to your project - See [CLI usage](../../guide/cli/index.md) for command-line options - Learn about [ignore comments](../../guide/ignore-syntax/index.md) for suppressing individual violations - Explore the [programmatic API](../../reference/api/index.md) to integrate with build tools or editors --- # /doc/src/content/CLAUDE.md > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/claude-md/doc--src--content **Path:** `doc/src/content/CLAUDE.md` # CLAUDE.md — src/content ## Bilingual Rule (EN + JA) When adding or modifying any documentation page, **always update both languages**: - English: `src/content/docs//index.mdx` - Japanese: `src/content/docs-ja//index.mdx` The JA directory mirrors the EN directory structure exactly. Every EN page has a corresponding JA page. ## Translation Rules - **Prose**: Translate to Japanese - **Code blocks**: Keep identical to EN — do NOT translate comments, strings, or identifiers in code blocks - **Frontmatter `title` and `description`**: Translate - **Frontmatter `category`, `sidebar_position`, `tags`**: Keep identical to EN - **Inline code (`` ` ``)**: Keep identical - **Link text**: Translate - **Link URLs**: Keep identical (they resolve to the same page under the appropriate locale) ## Internal Links Use **relative `.md` paths** for cross-page links, not absolute `/docs/...` paths. The `remark-resolve-markdown-links` plugin rewrites `.md`/`.mdx` extensions and applies the base prefix; bare absolute paths bypass it. ```md See the [methodology](../../reference/methodology/index.md) page. See the [methodology](/docs/reference/methodology) page. ``` External links (`https://...`) pass through unchanged. ## Frontmatter Schema ```yaml --- title: Page Title description: Short SEO/sidebar description sidebar_position: 1 category: guide --- ``` - `title` (required) - `description` (optional but recommended) - `sidebar_position` (optional, controls order within a category) - `category` (optional, groups pages in the sidebar and matches `headerNav.categoryMatch`) - `tags` (optional array) - `draft` (optional boolean — excluded from production builds) ## Categories and Header Nav Categories used in this project (match `settings.headerNav.categoryMatch` in `src/config/settings.ts`): - `overview` — What is?, Getting Started - `playground` — Playground - `guide` — Configuration, CLI, Ignore Syntax, Examples - `reference` — API, Methodology - `changelog` — Changelog - `claude` — auto-generated Adding a new category requires updating `headerNav` in `src/config/settings.ts`. ## Auto-generated Content `src/content/docs/claude/`, `src/content/docs/claude-md/`, `src/content/docs-ja/claude/`, and `src/content/docs-ja/claude-md/` are auto-generated by the `claude-resources` integration on every build. They are gitignored. Do not hand-edit them. --- # Methodology > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/reference/methodology `design-token-lint` enforces a specific approach to building design systems with Tailwind CSS. This page explains the reasoning. ## The Problem Tailwind ships with hundreds of utility classes and a default color palette. This is great for prototypes but creates problems at scale: 1. **Inconsistency creeps in silently.** A developer writes `p-4` here, `p-6` there, and `p-3` somewhere else. None are wrong. None are flagged. Design drifts. 2. **Refactoring is expensive.** When the design system changes — say, spacing base unit moves from `4px` to `6px` — every hardcoded `p-4` needs to be audited and possibly updated. 3. **Intent is lost.** `p-4` says "padding of 4 units" but doesn't say *why*. Is it card padding? Button inset? Section gutter? The class gives no clue. ## The Solution Replace raw numeric utilities with **semantic tokens**: ```diff - + ``` The semantic version: - Communicates intent (`surface`, `fg`, `md`, `sm`) - Can be re-tuned centrally without touching every component - Stays consistent because there's a finite vocabulary ## Token Categories ### Spacing Use `hgap-*` (horizontal) and `vgap-*` (vertical) suffixes: ``` p-hgap-sm # small horizontal padding m-vgap-lg # large vertical margin gap-hgap-md # medium gap ``` Scale names (`2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl`, ...) are defined in your Tailwind config and map to actual pixel values. ### Colors Use semantic names that describe role, not hue: ``` bg-surface # any surface (cards, panels) bg-surface-alt # alternate surface tint text-fg # primary foreground text-muted # de-emphasized text border-muted # subtle borders bg-accent # primary action color ``` Or use project-scoped tokens: ``` bg-zd-black # project-specific palette text-p7 # primary palette slot 7 ``` The exact names are up to your design system — the linter just blocks *default Tailwind colors* (`gray`, `blue`, `red`, etc.). ## What Still Works The linter isn't trying to ban Tailwind. Most utilities still work: - Layout: `flex`, `grid`, `block`, `hidden`, `w-full`, `h-screen` - Typography: `font-bold`, `text-lg`, `leading-tight`, `tracking-wide` - Effects: `shadow`, `rounded`, `opacity-50`, `transition` - Zero and 1px spacing: `p-0`, `m-0`, `gap-0`, `p-1px` - Arbitrary values: `w-[28px]`, `bg-[#123]`, `p-[10px]` Only **raw numeric spacing** and **default-palette colors** are flagged. ## Escape Hatches Sometimes you genuinely need a raw value. Use one of: 1. **Arbitrary values**: `p-[14px]` — explicit, searchable, reviewer-visible 2. **Ignore comment**: `{/* design-token-lint-ignore */}` on the preceding line — see [ignore syntax](../../guide/ignore-syntax/index.md) 3. **Allowlist**: add the class to your config's `allowed` array Prefer arbitrary values for one-offs. Reserve allowlist for legitimate exceptions that repeat. ## Further Reading This linter is a concrete enforcement of the broader [zudo-css-wisdom methodology](https://takazudomodular.com/pj/zudo-css-wisdom/docs/methodology/) — a set of patterns for building consistent, maintainable design systems with Tailwind CSS. Read that page for the full theoretical background. --- # Ignore Syntax > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/guide/ignore-syntax Sometimes you need to use a prohibited class for legitimate reasons — a third-party integration, a one-off experiment, or a deliberate escape hatch. Use an ignore comment to suppress violations on the next line. ## Syntax Place a `design-token-lint-ignore` comment on the line **immediately before** the line containing the violation. Three comment forms are recognized: ### JSX/TSX ```tsx {/* design-token-lint-ignore */} ``` ### CSS-in-JS / block comments ```tsx /* design-token-lint-ignore */ ``` ### Line comments ```tsx // design-token-lint-ignore ``` ## How It Works When the linter scans a file, it remembers ignore markers. For each violation, it checks whether the previous line is marked. If so, the violation is suppressed. Ignore comments only affect the **next line**. They do not suppress violations further down. ```tsx {/* design-token-lint-ignore */} {/* suppressed */} {/* NOT suppressed */} ``` ## When to Use ### Good reasons - **Third-party library integration** — when a component requires raw Tailwind classes for props - **Generated code** — auto-generated files you can't or shouldn't modify - **Temporary workaround** — documented with a comment explaining why ### Bad reasons - **"I don't feel like adding a semantic token"** — add the token - **Widespread use** — if you're ignoring the same class in many places, add it to the `allowed` list in config - **Silencing the linter entirely** — that defeats the purpose ## File-level Ignore To skip an entire file, add a `design-token-lint-ignore-file` comment anywhere in the file. The linter will produce zero violations for that file regardless of its content. ### JSX/TSX ```tsx {/* design-token-lint-ignore-file */} ``` ### CSS-in-JS / block comments ```tsx /* design-token-lint-ignore-file */ ``` ### Line comments ```tsx // design-token-lint-ignore-file ``` The comment can appear at the top of the file or anywhere inside it — the entire file is skipped either way. ### When to use file-level ignore - **Generated files** — auto-generated output you can't or shouldn't modify (e.g. icon sprites, Storybook story files, auto-generated wrappers) - **Legacy files under migration** — temporarily suppress a file while you work through a large codebase migration, then remove the comment when done - **Test fixtures** — snapshot or fixture files that intentionally contain raw utility classes Avoid using file-level ignore as a blanket suppressor for active source files. Prefer line-level ignore or the `allowed` config list for targeted exceptions. ## Alternatives Before reaching for an ignore comment, consider: 1. **Arbitrary value syntax**: `p-[14px]` is explicit and doesn't need an ignore 2. **Semantic token**: `p-hgap-md` if an appropriate scale exists 3. **Config allowlist**: add the class to `allowed` in `.design-token-lint.json` if it's a legitimate exception that repeats ## Documenting Ignores Ignore comments are invisible in diffs when reviewers aren't paying attention. Add a `why` comment so future maintainers understand: ```tsx {/* design-token-lint-ignore — third-party Calendar widget requires raw p-4 */} ``` --- # Examples > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/guide/examples 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: ```tsx // Developer A's component — uses p-4 // Developer B's component — uses p-5 for "medium padding" // Developer C's component — uses p-6 ``` 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: | Token | Value | Use | | --- | --- | --- | | `hsp-2xs` | 2px | tight inline spacing | | `hsp-xs` | 6px | compact inline | | `hsp-sm` | 8px | small padding | | `hsp-md` | 12px | default gaps | | `hsp-lg` | 16px | standard padding | | `hsp-xl` | 24px | generous padding | | `hsp-2xl` | 32px | large padding | **Vertical spacing** (`vsp-*`) — section gaps, vertical padding: | Token | Value | Use | | --- | --- | --- | | `vsp-2xs` | 7px | tight gap | | `vsp-xs` | 14px | small gap | | `vsp-sm` | 20px | compact gap | | `vsp-md` | 24px | standard gap | | `vsp-lg` | 28px | section gap | | `vsp-xl` | 40px | large section gap | | `vsp-2xl` | 56px | page-level gap | ### Color tokens Semantic names instead of palette shades: | Token | Tailwind class | Meaning | | --- | --- | --- | | `surface` | `bg-surface` | card/sidebar backgrounds | | `muted` | `text-muted` | secondary, de-emphasized text | | `accent` | `bg-accent` | primary interactive color | | `accent-hover` | `bg-accent-hover` | hover state for accent | | `code-bg` | `bg-code-bg` | inline code background | | `code-fg` | `text-code-fg` | inline code text | | `success` | `text-success` | positive states | | `danger` | `text-danger` | error / destructive states | | `warning` | `text-warning` | caution states | | `info` | `text-info` | informational 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: ```json { "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: | Class | Reason | Fix | | --- | --- | --- | | `p-4` | matches `p-{n}` | → `p-hsp-lg` (16px standard padding) | | `gap-2` | matches `gap-{n}` | → `gap-hsp-sm` (8px small gap) | | `bg-gray-200` | matches `bg-{color}-{shade}` | → `bg-surface` (surface background) | | `text-gray-500` | matches `text-{color}-{shade}` | → `text-muted` (secondary text) | | `bg-blue-600` | matches `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 ```tsx // ArticleCard.tsx — flagged by design-token-lint return ( {title} {excerpt} {tag} ) } ``` 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 ```tsx // ArticleCard.tsx — clean return ( {title} {excerpt} {tag} ) } ``` ### 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 ### Tight token project (recommended) A project where Tailwind defaults are fully replaced by semantic tokens: ```json { "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: ```json { "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 ```json { "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 ```json { "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: ```json { "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: ```yaml # .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](https://github.com/evilmartians/lefthook): ```yaml # 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: ```ts // scripts/lint-tokens.mjs 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: ```tsx {/* design-token-lint-ignore — vendor component requires literal p-4 */} ``` See [ignore syntax](../ignore-syntax/index.md) for all comment forms. --- # Claude > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/claude Claude Code configuration reference. ## Resources --- # v0.1.0 > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/changelog/v0.1.0 Initial release of `@zudolab/design-token-lint`. - CLI: `design-token-lint` with file/glob arguments and default scan patterns - Configurable prohibited patterns, allowed exceptions, ignored files via `.design-token-lint.json` - Pattern placeholders: `{n}`, `{color}`, `{shade}` - Ignore comments: `{/* design-token-lint-ignore */}` and variants - Programmatic API: `lintFile`, `lintContent`, `checkClass`, `extractClasses`, config loading utilities - Built-in default rules for Tailwind spacing and color utilities