# Design Token Lint > Forbids Tailwind classes that violate design token rules --- # 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}" ], "classAttributes": ["className", "class", "inputClassName", "wrapperClass"], "classFunctions": ["cn", "clsx", "classNames", "twMerge", "cva", "tv"] } ``` ## 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) | | `classAttributes` | `string[]` | HTML/JSX attribute names the extractor scans for class names | | `classFunctions` | `string[]` | Utility function names the extractor scans for class name arguments | 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` ### `classAttributes` An array of attribute names the extractor scans for class names. Any JSX/HTML attribute in this list is treated as a class attribute and its string value is extracted and linted. **Default**: `["className", "class"]` Use this when your project uses component libraries that accept class names through non-standard prop names: ```json { "classAttributes": ["className", "class", "inputClassName", "wrapperClass"] } ``` This is useful for libraries like Headless UI, Radix UI, or custom component libraries that pass class names via multiple props. > **Note**: `class:list` (Astro's directive syntax) is always scanned regardless of this setting. ### `classFunctions` An array of utility function names the extractor scans for class name arguments. Calls to these functions are extracted and their string arguments are linted. **Default**: `["cn", "clsx", "classNames", "twMerge"]` Use this to add support for additional class-merging utilities in your project: ```json { "classFunctions": ["cn", "clsx", "classNames", "twMerge", "cva", "tv", "twJoin"] } ``` This is useful when using libraries like `class-variance-authority` (`cva`), `tailwind-variants` (`tv`), or additional utilities from `tailwind-merge` such as `twJoin`. ## 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. `@takazudo/zudo-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 `@takazudo/zudo-design-token-lint` exports a small API for integration with build tools, editors, or custom tooling. ## Installation ```bash pnpm add @takazudo/zudo-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, DEFAULT_CLASS_ATTRIBUTES, DEFAULT_CLASS_FUNCTIONS, type ExtractedClass, type ExtractorOptions, } from '@takazudo/zudo-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, options?)` 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 } // ] ``` Accepts an optional `options` parameter to customize which attributes and functions are scanned: ```ts const extracted = extractClasses(content, { classAttributes: ['className', 'class', 'inputClassName'], classFunctions: ['cn', 'clsx', 'cva', 'tv'], }); ``` Returns `ExtractedClass[]`: ```ts interface ExtractedClass { className: string; line: number; } ``` Supported syntaxes by default: - `className="..."` and `class="..."` (JSX/Astro) - `className={'...'}` single-quote brace - `` className={`...`} `` template literals (simple cases) - `class:list={["...", '...']}` Astro class:list arrays (always scanned) - `cn(...)`, `clsx(...)`, `classNames(...)`, `twMerge(...)` utility calls ### `DEFAULT_CLASS_ATTRIBUTES` The default list of attribute names scanned by `extractClasses`: ```ts const DEFAULT_CLASS_ATTRIBUTES: string[]; // ["className", "class"] ``` ### `DEFAULT_CLASS_FUNCTIONS` The default list of utility function names scanned by `extractClasses`: ```ts const DEFAULT_CLASS_FUNCTIONS: string[]; // ["cn", "clsx", "classNames", "twMerge"] ``` ## Types ### `LintConfig` ```ts interface LintConfig { prohibited: string[]; allowed: string[]; ignore: string[]; patterns?: string[]; suggestionSuffix?: string; classAttributes?: string[]; classFunctions?: string[]; } ``` ### `ExtractorOptions` Options passed to `extractClasses()` to customize which attributes and functions are scanned. ```ts interface ExtractorOptions { classAttributes?: string[]; classFunctions?: string[]; } ``` Both fields are optional. When omitted, `DEFAULT_CLASS_ATTRIBUTES` and `DEFAULT_CLASS_FUNCTIONS` are used respectively. ### `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 '@takazudo/zudo-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 `@takazudo/zudo-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 @takazudo/zudo-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 `@takazudo/zudo-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 @takazudo/zudo-design-token-lint ``` Or with npm / yarn: ```bash npm install --save-dev @takazudo/zudo-design-token-lint yarn add --dev @takazudo/zudo-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. --- # Contribution > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/overview/contribution This project is a linter that enforces semantic design tokens instead of raw Tailwind numeric utilities. I originally built it because I wanted to enforce consistent design token usage across my projects. It started as a sub-package within one of my projects, but since I use a documentation tool for all of my projects, I found myself copying and pasting the package into every project directory. So I published it to npm for consistency and easier maintenance. Most of the source code in this project was written by [Claude Code](https://claude.ai/). While this is publicly available as an open-source project, I'm not sure whether I'll actively maintain the package going forward. Feel free to fork it or open pull requests, but I can't guarantee that I'll be able to review or merge them. Since we're already in the AI era, I'd recommend forking the repo and making whatever tweaks you need on your own. -- [@Takazudo](https://x.com/Takazudo) --- # 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 */} ``` --- # Known Limitations > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/reference/limitations `design-token-lint` uses regex-based static analysis to extract class names from source files. This approach is fast and requires no build step, but it cannot evaluate code at runtime. As a result, some dynamic patterns are not correctly analyzed. The set of attribute names and utility functions scanned is configurable via [`classAttributes` and `classFunctions`](../../guide/configuration/index.md) in your config file. By default, `className`, `class`, `cn`, `clsx`, `classNames`, and `twMerge` are scanned. These are not bugs — they are inherent to static analysis. ## Supported: Multiline className Multiline `className` values are supported: ```tsx ``` Classes spread across multiple lines are extracted correctly. ## Supported: Object Keys in class:list Astro's `class:list` object syntax is supported — class names in quoted keys are extracted: ```astro ``` Both `p-4` and `m-8` are extracted and linted. ## Limitation: Conditional Expressions Ternary expressions inside `className` are not extracted: ```tsx // Not linted — classes inside ternaries are silently skipped ``` The extractor looks for string literals directly assigned to `className` or `class`. The ternary syntax is not matched, so no classes are extracted — and no violations are reported. This means the linter will silently miss prohibited classes inside ternaries rather than producing false positives. There is no workaround for making the linter check inside ternaries. If you need to ensure these classes are linted, extract them into static variables: ```tsx // These static strings are linted const activeClass = "p-hgap-sm"; const inactiveClass = "m-vgap-md"; ``` ## Limitation: Template Interpolation Template literals with dynamic expressions produce garbled class names that never match any linting rules: ```tsx // Not linted — `p-${size}` is extracted as a literal string, matches no rules ``` The extractor captures the raw template content including `${...}` expressions. The resulting strings (`p-${size}`, `bg-${color}-500`) don't match any pattern, so violations inside them are never reported. **Workaround**: Add an ignore comment or refactor to use static class names: ```tsx // Use ignore comment to acknowledge this is intentional {/* design-token-lint-ignore */} ``` ## Limitation: Escaped Quotes Class attributes containing escaped quotes may extract incorrectly: ```tsx // May extract incorrectly ``` The regex parser does not handle escaped quote sequences inside string literals. **Workaround**: Avoid escaped quotes inside class attributes. Use JSX expression syntax instead: ```tsx ``` ## Summary | Pattern | Supported | | --- | --- | | Multiline `className` | Yes | | Static string literals | Yes | | Template literals (static only) | Yes | | Object keys in `class:list` | Yes | | Custom attribute names (via `classAttributes`) | Yes — configurable | | Custom utility functions (via `classFunctions`) | Yes — configurable | | Ternary expressions | No — silently skipped | | Template literals with interpolation | No — dynamic parts not linted | | Escaped quotes in class strings | No — may extract incorrectly | --- # 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.2.0 > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/changelog/v0.2.0 Adds configurable class extraction — you can now specify which attribute names and utility functions the extractor scans. - `classAttributes` config option: specify the attribute names (e.g. `inputClassName`, `wrapperClass`) the extractor should scan, replacing the defaults (`className` and `class` — include them in your list to keep them) - `classFunctions` config option: specify the utility function names (e.g. `cva`, `tv`, `twJoin`) the extractor should scan, replacing the defaults (`cn`, `clsx`, `classNames`, and `twMerge` — include them in your list to keep them) - `ExtractorOptions` type added to public API - `extractClasses()` now accepts an optional second parameter (`options?: ExtractorOptions`) to override attributes and functions per-call - `DEFAULT_CLASS_ATTRIBUTES` and `DEFAULT_CLASS_FUNCTIONS` constants exported for reference - Backwards compatible — no breaking changes; existing configs and code continue to work without modification --- # v0.1.0 > Source: https://takazudomodular.com/pj/zudo-design-token-lint/docs/changelog/v0.1.0 Initial release of `@takazudo/zudo-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