Stylelint Plugin Configuration: Enforcing Token-Only Values at the Lint Stage

Part of Token Scaling, Validation & CI Pipelines. This page covers the precise mechanics of wiring Stylelint — and custom plugins built on its PostCSS AST — to block hardcoded values and enforce token-only CSS declarations across every pull request.

The Problem This Solves

Design systems erode when engineers write color: #1e40af instead of color: var(--ds-color-action-primary). Hardcoded values bypass the token contract silently — no build error, no visual test failure until the theme changes. By the time a design ops team detects the drift, dozens of components may have accumulated raw hex codes, rgba() calls, or unitless shadows that are invisible to the token pipeline.

Stylelint, configured with token-aware rules, closes this gap at the earliest possible moment: the local pre-commit hook and the PR gate. No hardcoded value reaches the main branch without a deliberate override.

Stylelint token enforcement in the lint stage Sequence diagram showing CSS declarations flowing through Stylelint plugin rules: hardcoded values are blocked, valid token references pass to build. CSS Source component.css PostCSS AST Declaration nodes Plugin Rules token manifest check Hardcoded Value color: #1e40af; background: rgba(0,0,0,0.5); Token Reference color: var(--ds-color-action-primary); background: var(--ds-overlay-modal); Rule Engine walkDecls() token manifest regex validators severity: error BLOCKED exit 1 / PR gate PASSES proceeds to build Pre-commit hook + CI pull-request gate enforce identical rules
Stylelint's plugin rule engine intercepts PostCSS Declaration nodes, blocks hardcoded values at lint time, and passes only valid token references to the build.

Architectural Foundation for Token Linting

At the architectural level, Stylelint operates on a PostCSS AST (Abstract Syntax Tree). When a stylesheet is ingested, the parser tokenizes declarations, at-rules, and selectors into a traversable node graph. Token-aware plugins intercept Declaration nodes, extract the prop and value properties, and validate them against a centralized design token manifest. This AST-driven approach enables framework-agnostic enforcement: whether your stack relies on vanilla CSS, CSS-in-JS, or preprocessor outputs, the linting boundary remains consistent.

Baseline configuration scaffolding should prioritize deterministic resolution. Avoid global overrides that bypass the cascade; instead, leverage Stylelint’s ignoreFiles and files directives to scope validation strictly to design system entry points and component libraries. This prevents false positives in legacy codebases while maintaining strict compliance in active development zones.

Three-Tier Architectural Trade-Offs

Stylelint token enforcement decisions compound across three layers. Each trade-off pair below names a real engineering tension — choose deliberately rather than defaulting:

  • Strictness vs. adoption speed — Blocking all non-tokenized values on day one is correct for greenfield repos; it breaks adoption in legacy codebases. Use severity: warning with a max-warnings 0 ratchet that tightens per sprint instead of flipping to error globally.
  • Monolithic vs. modular plugin distribution — A single @org/stylelint-config-tokens package is easy to distribute but couples all teams to one release cadence. Per-domain packages (@org/stylelint-config-tokens-color, …-spacing) let teams opt into the scopes they own, at the cost of dependency graph complexity.
  • Single-level vs. recursive var() resolution — Single-level checks (var(--ds-color-action-primary) → present in manifest) are fast and sufficient for most rules. Recursive resolution catches aliased tokens (var(--comp-btn-bg)var(--ds-color-action-primary)) but adds 20–40 ms per file. Cache the resolved map; do not recompute per declaration.
  • Synchronous CI blocking vs. async reporting — Synchronous PR gates are non-negotiable for token compliance. Use --fix in pre-commit hooks to autocorrect mechanical violations (wrong namespace prefix) before they reach CI, reserving the gate for errors a fixup cannot resolve automatically.
  • Shared org preset vs. per-project overrides — Inheriting from a versioned org preset ("extends": ["@org/stylelint-config-tokens"]) guarantees baseline coverage. Project-level .stylelintrc.json overrides should only add ignoreFiles for local legacy directories, never relax error severities.

Plugin Configuration & Setup Workflow

The configuration workflow begins with defining plugin presets that map directly to your design system’s token registry. To prevent drift, pair this setup with JSON Schema validation for design tokens during the build phase. This ensures that variable declarations strictly conform to expected data types and naming conventions. Engineers should implement a hierarchical .stylelintrc.json structure that inherits from a shared organizational preset while allowing project-specific overrides.

A production-ready configuration enforces token namespace isolation, blocks hardcoded fallbacks, and mandates semantic naming patterns:

{
  "extends": [
    "stylelint-config-recommended",
    "stylelint-config-standard"
  ],
  "plugins": [
    "stylelint-value-no-unknown-custom-properties"
  ],
  "rules": {
    "custom-property-pattern": "^--(ds|theme|comp)-[a-z0-9-]+$",
    "value-no-unknown-custom-properties": [true, {
      "severity": "error",
      "ignoreProperties": ["/^--legacy-/"]
    }],
    "declaration-property-value-disallowed-list": {
      "/color|background|border/": [
        "/^#[0-9a-f]{3,8}$/i",
        "/^rgba?\\(/i",
        "/^hsl[a]?\\(/i"
      ]
    }
  },
  "ignoreFiles": ["**/node_modules/**", "**/vendor/**", "**/*.min.css"]
}

Notes on plugins used:

  • stylelint-value-no-unknown-custom-properties: A published Stylelint plugin that validates var() references against a known allowlist.
  • declaration-property-value-disallowed-list: A built-in Stylelint rule that blocks specific values for specific properties — no plugin required.

Build pipeline steps:

  1. Install stylelint, stylelint-config-recommended, stylelint-config-standard, and stylelint-value-no-unknown-custom-properties.
  2. Export the compiled token manifest from your token compiler (Style Dictionary, Cobalt, etc.) as a JSON file that the plugin reads at lint time.
  3. Define custom property regex patterns matching design token namespaces (--ds-, --theme-, --comp-).
  4. Wrap the config in a versioned scoped npm package (@org/stylelint-config-tokens) and publish to your internal registry.
  5. Add "lint:css": "stylelint '**/*.css'" to package.json scripts; configure Husky to run it as a pre-commit hook.
  6. Validate that the CI runner resolves node_modules correctly before executing the lint step.

For teams needing AST-level enforcement beyond what published plugins provide, writing custom Stylelint rules for token usage covers building walkDecls()-based plugins that resolve nested var() chains and validate scoped namespace constraints.

Distribute this configuration as a scoped npm package to ensure version-controlled consistency across micro-frontends and monorepo workspaces.

Validation & Quality Gates

Automated validation pipelines must intercept pull requests and run static analysis against the tokenized stylesheet. When integrated with Design-to-Code Sync Workflows, the linter acts as a gatekeeper, rejecting non-compliant CSS variables and flagging deprecated token references.

The following GitHub Actions configuration demonstrates a production-grade CI gate that enforces zero-error thresholds and publishes structured artifacts for audit compliance:

name: Token Lint & Validation
on:
  pull_request:
    paths:
      - '**/*.css'
      - '**/*.scss'
      - '.stylelintrc.json'
      - 'package.json'

jobs:
  stylelint:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - name: Run Stylelint
        run: |
          npx stylelint "**/*.css" \
            --formatter json \
            --output-file lint-report.json \
            --ignore-pattern "**/node_modules/**" \
          || true
      - name: Threshold Enforcement
        run: |
          ERROR_COUNT=$(node -e "
            const r = require('./lint-report.json');
            const errs = r.flatMap(f => f.warnings.filter(w => w.severity === 'error'));
            console.log(errs.length);
          ")
          if [ "$ERROR_COUNT" -gt 0 ]; then
            echo "Blocking merge: $ERROR_COUNT token violations detected."
            exit 1
          fi
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: stylelint-reports
          path: lint-report.json

Note: Stylelint’s JSON formatter outputs results[].warnings[] with a severity field of "warning" or "error". The threshold enforcement script above correctly flattens across files and filters by severity.

Tool table:

Tool Purpose Integration Point
stylelint AST-based CSS linting engine Pre-commit hook + CI PR gate
stylelint-value-no-unknown-custom-properties Validates var() references against manifest .stylelintrc.json plugins array
@org/stylelint-config-tokens Versioned org preset with all token rules extends in project config
Husky + lint-staged Run lint only on changed files pre-commit package.json prepare script

Pipeline compliance & performance targets:

  • Execution time: < 2s for monorepo baseline via file-level chunking
  • Memory footprint: < 150MB during AST traversal
  • Failure threshold: Block merges if error count exceeds zero

Cross-Cluster Dependency Mapping

Stylelint enforcement sits downstream of token compilation and upstream of visual regression and release pipelines. The table below maps integration points and the validation strategy at each boundary:

Parent Pillar Sibling Workflow Integration Point Validation Strategy
Token Scaling & CI Pipelines Automated Token Audit Scripts Lint runs before audit; audit detects orphaned tokens after lint pass Lint blocks new hardcoded values; detecting orphaned and unused tokens in CI then confirms no drift in the token manifest itself
Token Scaling & CI Pipelines JSON Schema Validation for Tokens Stylelint consumes the compiled token manifest; JSON Schema validates the source token JSON Schema validation runs in a parallel CI job; lint job depends on compiled manifest artifact
Token Scaling & CI Pipelines Design-to-Code Sync Workflows Figma export triggers token recompilation; updated manifest is committed before lint runs PR automation validates that manifest changes are accompanied by passing lint on all consuming CSS
Token Scaling & CI Pipelines Versioning & Semantic Release for Tokens Lint config version pins must advance when breaking token renames land Semantic release changelog entry references Stylelint config minor/major bump

In CSS, declare the dependency explicitly with a comment annotation so grep-based audits can trace the relationship:

/* @depends: @org/stylelint-config-tokens@^3.0.0, token-manifest.json@>=2024-01 */

.btn-primary {
  background-color: var(--ds-color-action-primary);
  color: var(--ds-color-text-on-action);
  padding: var(--ds-space-inset-md);
}

Advanced Architecture & Custom Rule Generation

For enterprise-grade systems, extending the base configuration requires programmatic rule generation. Custom plugins should leverage PostCSS AST traversal to validate token consumption patterns across component boundaries.

Architecture patterns for token enforcement:

  • Rule Factory Pattern: Dynamically generate lint rules from a centralized token manifest. A build script reads the design token JSON, compiles regex validators, and injects them into the Stylelint config at runtime. This keeps rule definitions in sync with the token registry without manual maintenance.
  • Visitor Pattern: Traverse CSS AST nodes to validate token references against allowed scopes. Implement walkDecls() to intercept var() functions, resolve nested variables, and verify they belong to permitted component or global namespaces.
// stylelint-plugin-token-scope.js
const stylelint = require('stylelint');
const tokenManifest = require('./token-manifest.json');

const ruleName = 'token-scope/no-out-of-scope';
const allowedTokens = new Set(Object.keys(tokenManifest));

module.exports = stylelint.createPlugin(ruleName, (primaryOption) => {
  return (root, result) => {
    const varPattern = /var\((--[a-z0-9-]+)/g;

    root.walkDecls((decl) => {
      let match;
      while ((match = varPattern.exec(decl.value)) !== null) {
        const tokenName = match[1];
        if (!allowedTokens.has(tokenName)) {
          stylelint.utils.report({
            ruleName,
            result,
            node: decl,
            message: `Token "${tokenName}" is not defined in the manifest.`,
            word: tokenName,
          });
        }
      }
    });
  };
});

module.exports.ruleName = ruleName;

This plugin reads the compiled token manifest at load time so the allowed-token set is built once per lint run, not per declaration.

Diagnostic Matrix

When token lint rules produce unexpected results — false positives, missed violations, or CI failures that do not reproduce locally — work through this matrix before escalating:

Diagnostic Step Execution Detail
Confirm Stylelint version parity Run npx stylelint --version locally and in CI. Mismatched versions produce different AST node shapes. Pin the version in devDependencies with an exact specifier.
Verify manifest path resolution Print process.cwd() inside your plugin and confirm the token manifest JSON path resolves from that directory. CI runners often change the working directory between steps.
Inspect raw AST output Add root.walkDecls(d => console.log(d.prop, d.value)) temporarily to confirm the parser is reaching the declaration you expect. Minified or concatenated CSS can produce multi-declaration strings.
Check ignoreFiles glob scope Run npx stylelint --print-config path/to/file.css to confirm the file inherits the expected config. Glob mismatches silently exclude files.
Reproduce in isolation Create a minimal test.css with one failing declaration and run npx stylelint test.css. Eliminates caching and glob issues.
Validate plugin registration Confirm the plugin export matches the rule name string in plugins[]. A mismatched name causes silent no-op rather than an error.

Root causes and resolutions:

Symptom Root Cause Resolution
Rules fire on --legacy- tokens marked for ignore ignoreProperties regex is missing the anchoring /^--legacy-/ pattern Add ^ anchor: "/^--legacy-/" in the ignore array
CI passes but local hook blocks Different Stylelint config resolved — local .stylelintrc.json overrides a parent Run --print-config in both environments; align the extends chain
var() references to valid tokens flagged as unknown Token manifest not updated after a token rename in the design tool Re-run the token compiler and commit the updated manifest before the lint step
Plugin exits with Cannot find module Scoped npm package not installed in CI node_modules Add @org/stylelint-config-tokens to devDependencies; confirm npm ci runs before lint
Lint passes with hardcoded values in :root declaration-property-value-disallowed-list only targets component-level selectors in config Expand the property pattern to `/color

Frequently Asked Questions

How do I migrate a legacy codebase without blocking all engineers on day one?

Switch all token rules to severity: warning and add --max-warnings 0 to the CI command. This lets engineers see violations without blocking merges initially. Each sprint, address one category of warning (e.g., all color violations), promote it to error, and tighten the threshold. Document the ratchet schedule so teams know the deadline. Most codebases can reach full compliance within three to six sprints this way.

Can Stylelint validate tokens in CSS-in-JS or Tailwind arbitrary values?

Stylelint parses the CSS output, not the source template. For CSS-in-JS (styled-components, Emotion), use @stylelint/postcss-css-in-js to parse the embedded CSS. Tailwind arbitrary values (text-[var(--ds-color-action-primary)]) are visible to Stylelint if you run it on the compiled output or use a Tailwind-aware PostCSS plugin. Validate at the compiled layer rather than the source template layer for consistent coverage across frameworks.

Should the token manifest live in the Stylelint config repo or the token package?

The manifest belongs in the token package, published as a package export: "exports": { "./manifest": "./dist/token-manifest.json" }. The Stylelint config imports it as require('@org/design-tokens/manifest'). This way, updating the token package version automatically updates what the linter validates, without requiring a separate config bump. Version coupling is intentional — a token rename must bump the token package minor version, which triggers a config update.