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.
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: warningwith amax-warnings 0ratchet that tightens per sprint instead of flipping toerrorglobally. - Monolithic vs. modular plugin distribution — A single
@org/stylelint-config-tokenspackage 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
--fixin 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.jsonoverrides should only addignoreFilesfor 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 validatesvar()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:
- Install
stylelint,stylelint-config-recommended,stylelint-config-standard, andstylelint-value-no-unknown-custom-properties. - Export the compiled token manifest from your token compiler (Style Dictionary, Cobalt, etc.) as a JSON file that the plugin reads at lint time.
- Define custom property regex patterns matching design token namespaces (
--ds-,--theme-,--comp-). - Wrap the config in a versioned scoped npm package (
@org/stylelint-config-tokens) and publish to your internal registry. - Add
"lint:css": "stylelint '**/*.css'"topackage.jsonscripts; configure Husky to run it as a pre-commit hook. - Validate that the CI runner resolves
node_modulescorrectly 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:
< 2sfor monorepo baseline via file-level chunking - Memory footprint:
< 150MBduring 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 interceptvar()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.
Related
- Token Scaling, Validation & CI Pipelines — parent section covering the full validation and CI architecture
- Writing Custom Stylelint Rules for Token Usage — deep dive on building
walkDecls()-based plugins for complex namespace and scope constraints - Detecting Orphaned and Unused Tokens in CI — the complementary audit pass that catches tokens no component references after Stylelint enforces correct token usage
- JSON Schema Validation for Tokens — validates the token source files that the Stylelint manifest is compiled from
- Design-to-Code Sync Workflows — upstream pipeline that compiles Figma exports into the token manifest Stylelint consumes