Writing Custom Stylelint Rules for Token Usage

Architecting Custom Stylelint Rules for Token Validation

Enforcing consistent design token consumption across large-scale frontends requires moving beyond static configuration. By authoring targeted linting rules, engineering teams can intercept invalid token references before they reach production. This architectural shift transforms CSS validation from a passive check into an active governance layer, ensuring that component styling remains strictly bound to the centralized design system registry. Integrating these custom validators directly into broader Token Scaling, Validation & CI Pipelines initiatives guarantees that CSS architecture scales predictably while maintaining strict alignment with the source-of-truth token manifest.

Step-by-Step Implementation: Rule Authoring

Begin by scaffolding a custom plugin using the Stylelint API. Define a rule name following the plugin/ namespace convention to avoid collisions with core rules. Implement the core validation logic using PostCSS AST traversal to inspect value and declaration nodes efficiently. Match token patterns using strict regex boundaries or JSON schema lookups against your design tokens manifest. When violations occur, return a stylelint.utils.report() call that includes precise line/column metadata and contextual messaging. Proper configuration of this workflow is documented in the Stylelint Plugin Configuration reference.

Production-Viable Rule Implementation:

// lib/rules/token-usage.js
const stylelint = require('stylelint');

const ruleName = 'plugin/token-usage';
const messages = stylelint.utils.ruleMessages(ruleName, {
 rejected: (token) => `Unregistered design token: "${token}"`,
});

module.exports = stylelint.createPlugin(ruleName, (primaryOption, secondaryOptions, context) => {
 return (root, result) => {
 const validOptions = stylelint.utils.validateOptions(result, ruleName, { actual: primaryOption });
 if (!validOptions) return;

 // Strict token pattern: var(--ds-[namespace]-[token-name])
 const tokenPattern = /^var\(--ds-([a-z0-9-]+)\)$/i;
 const registry = new Set(primaryOption.registry || []);

 root.walkDecls((decl) => {
 const match = decl.value.match(tokenPattern);
 if (match && !registry.has(match[1])) {
 stylelint.utils.report({
 message: messages.rejected(decl.value),
 node: decl,
 result,
 ruleName,
 word: decl.value,
 });
 }
 });
 };
});

module.exports.ruleName = ruleName;
module.exports.messages = messages;

CI Pipeline Integration and Execution

Integrate the custom rule into your CI workflow by registering it in the .stylelintrc.json plugins array and configuring the rule severity. Use stylelint --cache with a dedicated .stylelintcache directory to optimize execution times in monorepo environments, skipping unchanged files across incremental builds. Configure parallel execution across component packages using CI matrix strategies or turbo run lint to prevent pipeline bottlenecks. Set the rule to severity: "error" to fail the build on violations, while allowing severity: "warning" during gradual adoption phases. Monitor CI logs for stylelint exit codes (1 for errors, 0 for clean) to automate PR gating.

Diagnostic Workflows and Root Cause Analysis

When custom rules fail silently or produce false positives, isolate the issue using stylelint --debug. This flag outputs verbose AST parsing logs, plugin resolution paths, and rule execution traces. Common root causes include malformed PostCSS AST nodes, incorrect regex boundaries that inadvertently match utility classes, or asynchronous token resolution failures in the plugin loader. Resolve AST mismatches by explicitly defining the parser via CLI flags or config (syntax: "postcss-scss" or postcss-less). Fix false positives by implementing strict token namespace validation and adding explicit ignore patterns for vendor-prefixed properties or dynamic runtime values injected by CSS-in-JS runtimes.

Legacy Migration and Rollout Strategy

Migrating legacy codebases requires a phased enforcement strategy to avoid blocking developer velocity. Start with severity: "warning" and generate a baseline violation report using stylelint --formatter json > violations.json. Use automated scripts to batch-replace deprecated token aliases with canonical references, leveraging AST-aware codemods to preserve selector specificity. Gradually escalate to severity: "error" once coverage exceeds 90% and the remaining violations are isolated to deprecated vendor components. Maintain a centralized changelog mapping deprecated tokens to their replacements, and expose this mapping via a CLI utility to streamline developer onboarding and reduce friction during cross-team refactors.

Troubleshooting Matrix

Diagnostic Steps

  1. Run stylelint --debug to capture AST parsing logs and plugin resolution paths.
  2. Validate token manifest against JSON schema before rule execution to prevent runtime parsing errors.
  3. Isolate failing files using --stdin-filename for targeted debugging in CI environments.
  4. Check plugin resolution path in node_modules symlink structures to prevent MODULE_NOT_FOUND errors.

Root Causes and Resolutions

Symptom Root Cause Resolution
Rule executes but reports zero violations despite invalid tokens Incorrect PostCSS node traversal scope or missing walkDecls callback Verify AST structure with postcss.parse(), ensure recursive traversal includes nested selectors and at-rules
CI pipeline fails with MODULE_NOT_FOUND for custom plugin Plugin not published to registry or incorrect relative path in config Use absolute path resolution via path.resolve(__dirname, './plugins') or publish to internal npm registry
False positives on CSS variables or dynamic values Overly permissive regex matching non-token var() references Implement strict prefix/suffix validation (e.g., ^--ds-), add explicit ignore patterns for dynamic runtime values