
Security News
Open VSX Unblocks Extension IDs Used in Malware Campaign
Open VSX has removed three extension IDs from its malicious-extension list as the legitimate publishers they impersonated move to claim the names for themselves.
stylelint-plugin-rhythmguard
Advanced tools
Token governance for CSS and Tailwind — enforce spacing scales, require design tokens, catch arbitrary values
Token governance for CSS and Tailwind. Enforce spacing scales, require design tokens, and catch arbitrary values before they ship.
Rhythmguard enforces scale and token discipline across spacing, radius, typography, size, and motion offsets — in CSS declarations and Tailwind class strings.
Built for teams that want:
p-[13px] → p-[12px])npm install --save-dev stylelint stylelint-plugin-rhythmguard
.stylelintrc.json:
{
"extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
}
eslint.config.js (for Tailwind class-string governance):
import rhythmguard from 'stylelint-plugin-rhythmguard/eslint';
export default [
{
plugins: { 'rhythmguard-tailwind': rhythmguard },
rules: {
'rhythmguard-tailwind/tailwind-class-use-scale': [
'error',
{ scale: [0, 4, 8, 12, 16, 24, 32] }
],
},
},
];
This gives you spacing governance in both CSS files and JSX/TSX templates.
| Rule | What it does | Autofix |
|---|---|---|
rhythmguard/use-scale | Enforces spacing values must be on your configured scale | Yes, nearest safe value |
rhythmguard/prefer-token | Enforces token usage over raw spacing literals | Yes, with tokenMap |
rhythmguard/no-offscale-transform | Enforces scale-aligned translate* motion offsets | Yes, nearest safe value |
rhythmguard/use-motion-scale | Enforces opt-in duration/delay rhythm and flags raw easing curves | Yes, for duration/delay values |
I built Rhythmguard after 20 years of watching teams ignore spacing scales and ship arbitrary pixel values everywhere.
petrilahdelma.github.io/stylelint-plugin-rhythmguard — paste CSS, see violations and token opportunities live. No install, no config.
Use the audit CLI to create a design-system drift report before turning rules into hard CI gates:
npx rhythmguard audit ./src
npx rhythmguard audit ./src --format markdown
npx rhythmguard audit ./src --json
npx rhythmguard audit . --ignore "apps/legacy/**" --ignore "vendor/**"
npx rhythmguard audit ./src --write-baseline
npx rhythmguard audit ./src --since-baseline --fail-on-new-drift
npx rhythmguard audit ./src --staged --max-findings 0
npx rhythmguard audit ./src --token-source ./tokens.json
npx rhythmguard audit ./src --token-source ./theme.css --token-source-format css
npx rhythmguard audit ./src --include-motion
npx rhythmguard audit ./src --format html --output rhythmguard-report.html
npx rhythmguard audit --schema
The report covers authored CSS declarations, Tailwind arbitrary spacing values in common template/source files, and token-contract drift such as missing spacing tokens, unused spacing tokens, repeated raw values that deserve token review, raw values that match known tokens, conflicting token values, and opt-in motion rhythm drift. Scan paths are scoped to the directory argument. Use --ignore, .rhythmguardignore, or --ignore-path for generated or legacy subtrees, then add baselines and CI thresholds when you are ready to gate new drift. Markdown output is PR-ready for UX developers, UX designers, and design-system owners:
# Rhythmguard Design-System Audit
| Metric | Value |
| --- | ---: |
| CSS files scanned | 47 |
| Template files scanned | 83 |
| Files with issues | 12 |
| Total findings | 52 |
| Scale cleanliness | 91% |
| New findings | 3 |
For large codebases, put shared audit settings in .rhythmguardrc.json:
{
"audit": {
"ignore": ["legacy/**", "generated/**"],
"tokenSources": [
"./tokens.json",
{ "path": "./src/theme.css", "format": "css" }
],
"tokenKind": "spacing",
"includeMotion": false,
"tokenCandidateMinCount": 2,
"minCleanliness": 90
}
}
rhythmguard audit loads .rhythmguardrc.json automatically when present. Use --config <file> for another config, --no-config to skip config discovery, and --token-source <file> for extra canonical token files. Token source paths in config files resolve from the config file directory; CLI token source paths resolve from the current working directory. Supported source formats are CSS custom properties and Tailwind v4 @theme, flat JSON maps, Style Dictionary JSON, and DTCG JSON.
In Rhythmguard 2.0, --format json emits the stable audit contract:
{
"schemaVersion": "2.0",
"command": { "directory": "./src", "scanScope": "full" },
"summary": { "totalFindings": 12, "scaleCleanliness": 94 },
"scanned": { "cssFiles": 10, "templateFiles": 20 },
"contracts": {
"scale": {},
"tokens": {},
"motion": {}
},
"findings": {
"css": [],
"tailwind": [],
"motion": []
},
"baseline": null
}
Use --format json-v1 for the pre-2.0 JSON shape during migration.
Programmatic usage:
const {
createAuditReport,
toAuditContractReport,
} = require('stylelint-plugin-rhythmguard/audit');
const report = await createAuditReport({ dir: './src', noConfig: true });
const contract = toAuditContractReport(report);
npm install --save-dev stylelint stylelint-plugin-rhythmguard
If your project already uses Stylelint, you only need one command and one config block:
npm install --save-dev stylelint-plugin-rhythmguard
{
"extends": ["stylelint-plugin-rhythmguard/configs/recommended"]
}
{
"extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
}
{
"extends": ["stylelint-plugin-rhythmguard/configs/recommended"]
}
{
"extends": ["stylelint-plugin-rhythmguard/configs/strict"]
}
strict intentionally delegates transform translation enforcement to rhythmguard/no-offscale-transform to reduce overlapping warnings from use-scale.
{
"extends": ["stylelint-plugin-rhythmguard/configs/expanded"]
}
expanded enables scale enforcement for spacing + radius + typography + size property groups.
{
"extends": ["stylelint-plugin-rhythmguard/configs/logical"]
}
logical composes Rhythmguard strict mode with stylelint-plugin-logical-css recommended rules.
{
"extends": ["stylelint-plugin-rhythmguard/configs/migration"]
}
migration keeps on-scale numeric values temporarily while auto-building token mappings from CSS custom properties and optional Tailwind spacing config.
{
"extends": ["stylelint-plugin-rhythmguard/configs/react-tailwind"]
}
react-tailwind extends the tailwind config with CSS Modules overrides (spacing + radius enforcement) and ignores Next.js build directories.
{
"extends": ["stylelint-plugin-rhythmguard/configs/motion"]
}
motion enables opt-in duration/delay rhythm checks with rhythmguard/use-motion-scale.
Stable shared config entry points:
stylelint-plugin-rhythmguard/configs/recommendedstylelint-plugin-rhythmguard/configs/strictstylelint-plugin-rhythmguard/configs/tailwindstylelint-plugin-rhythmguard/configs/react-tailwindstylelint-plugin-rhythmguard/configs/expandedstylelint-plugin-rhythmguard/configs/logicalstylelint-plugin-rhythmguard/configs/migrationstylelint-plugin-rhythmguard/configs/motionFramework-specific setup for Vue, Lit, Astro, and SvelteKit: docs/FRAMEWORKS.md
docs/COMPARISON.mddocs/AUDIT_2_VALIDATION.mddocs/CI_ADOPTION.mddocs/AUDIT_API_EXAMPLES.mddocs/ADOPTION_DIFFS.mddocs/DISTRIBUTION.md{
"plugins": ["stylelint-plugin-rhythmguard"],
"rules": {
"rhythmguard/use-scale": [
true,
{
"preset": "rhythmic-4",
"propertyGroups": ["spacing", "radius"],
"propertyScales": {
"font-size": [12, 14, 16, 20, 24]
},
"units": ["px", "rem", "em"],
"unitStrategy": "convert",
"baseFontSize": 16,
"tokenPattern": "^--space-",
"tokenFunctions": ["var", "theme", "token"],
"allowNegative": true,
"allowPercentages": true,
"fixToScale": true,
"enforceInsideMathFunctions": true,
"mathFunctionArguments": {
"clamp": [1, 3]
}
}
],
"rhythmguard/prefer-token": [
true,
{
"tokenPattern": "^--space-",
"allowNumericScale": false,
"tokenMapFromCssCustomProperties": true,
"tokenMapFromTailwindSpacing": true,
"tailwindConfigPath": "./tailwind.config.mjs",
"tokenMap": {
"4px": "var(--space-1)",
"8px": "var(--space-2)",
"12px": "var(--space-3)",
"16px": "var(--space-4)"
}
}
],
"rhythmguard/no-offscale-transform": [
true,
{
"scale": [0, 4, 8, 12, 16, 24, 32]
}
]
}
}
Preset-based setup:
{
"rules": {
"rhythmguard/use-scale": [true, { "preset": "fibonacci" }]
}
}
Custom scale setup:
{
"rules": {
"rhythmguard/use-scale": [true, { "customScale": [0, 6, 12, 18, 24, 36, 48] }]
}
}
Scale resolution precedence:
customScale (highest priority)scalepresetrhythmic-4 scaleRhythmguard validates secondaryOptions for each rule before linting declarations.
properties string entries are validated against supported scale-targetable CSS property names.propertyGroups values are validated against built-in groups: spacing, radius, typography, and size.calc, clamp, min, max) and positive 1-based argument indexes.Example typo that now fails immediately:
{
"rules": {
"rhythmguard/use-scale": [true, { "sevverity": "warning" }]
}
}
| Preset | Pattern | Scale |
|---|---|---|
rhythmic-4 | 4pt rhythm | [0,4,8,12,16,24,32,40,48,64] |
rhythmic-8 | 8pt rhythm | [0,8,16,24,32,40,48,64,80,96] |
product-material-8dp | Material 8dp baseline + 4dp increments | [0,4,8,12,16,24,32,40,48,56,64,72,80] |
product-atlassian-8px | Atlassian-like product spacing progression | [0,2,4,6,8,12,16,20,24,32,40,48,64,80] |
product-carbon-2x | Carbon 2x spacing progression | [0,2,4,8,12,16,24,32,40,48,64,80] |
editorial-baseline-4 | editorial baseline rhythm at 4-unit cadence | [0,4,8,12,16,20,24,28,32,40,48,56,64] |
editorial-baseline-6 | editorial baseline rhythm at 6-unit cadence | [0,6,12,18,24,30,36,48,60,72] |
compact | dense UI spacing | [0,2,4,6,8,12,16,20,24,32] |
fibonacci | Fibonacci progression | [0,2,3,5,8,13,21,34,55,89] |
powers-of-two | geometric doubling | [0,2,4,8,16,32,64,128] |
golden-ratio | ratio 1.618 | generated modular sequence |
modular-major-second | ratio 1.125 | generated modular sequence |
modular-minor-third | ratio 1.2 | generated modular sequence |
modular-major-third | ratio 1.25 | generated modular sequence |
modular-augmented-fourth | ratio 1.414 | generated modular sequence |
modular-perfect-fourth | ratio 1.333 | generated modular sequence |
modular-perfect-fifth | ratio 1.5 | generated modular sequence |
Aliases:
4pt → rhythmic-48pt → rhythmic-8material → product-material-8dpatlassian-8 → product-atlassian-8pxcarbon → product-carbon-2xbaseline-4 → editorial-baseline-4baseline-6 → editorial-baseline-6golden → golden-ratiomajor-second → modular-major-secondminor-third → modular-minor-thirdmajor-third → modular-major-thirdaugmented-fourth → modular-augmented-fourthperfect-fourth → modular-perfect-fourthperfect-fifth → modular-perfect-fifthdocs/SCALE_RESEARCH.md.Rhythmguard supports community-contributed scale presets from scales/community/*.json.
| Preset | Base | Pattern | Contributor |
|---|---|---|---|
product-decimal-10 | 10 | Decimal-friendly dashboard/product cadence | Petri Lahdelma |
npm run scales:add -- --name my-team-scale --base 8 --steps 0,4,8,12,16,24,32
npm run scales:validate
Full specification and policy: docs/COMMUNITY_SCALES.md.
If your scale is private or very niche, keep it in your project config with customScale instead of contributing it to the shared registry.
rhythmguard/use-scaleEnforces spacing literals to stay on a configured numeric scale.
Checks:
margin*, padding*gap, row-gap, column-gapinset*, scroll-margin*, scroll-padding*translate, translate-x, translate-y, translate-ztransform translation functions (translate, translateX, translateY, translateZ, translate3d)radius (border-radius*, corner radii, outline-offset)typography (font-size, line-height, letter-spacing, word-spacing)size (width, height, min/max size, logical inline-size/block-size)Example:
/* ❌ Off-scale */
.card {
margin: 13px;
transform: translateY(18px);
}
/* ✅ On-scale */
.card {
margin: 12px;
transform: translateY(16px);
}
Options:
| Option | Type | Default | Description |
|---|---|---|---|
preset | string | rhythmic-4 | Selects a built-in spacing scale |
customScale | `Array<number | string>` | undefined |
scale | `Array<number | string>` | [0,4,8,12,16,24,32,40,48,64] |
units | string[] | ['px','rem','em'] | Units considered for scale enforcement |
unitStrategy | 'convert' | 'exact' | 'convert' | convert: compare via px conversion (px/rem/em). exact: compare against same-unit scale values (for example vw, cqi) |
baseFontSize | number | 16 | Used for rem/em conversion |
tokenPattern | string | ^--space- | Regex for accepted token variable names |
tokenFunctions | string[] | ['var','theme','token'] | Functions treated as tokenized values |
allowNegative | boolean | true | Allows negative scale values |
allowPercentages | boolean | true | Allows % values without scale checks |
fixToScale | boolean | true | Enables nearest-value autofix |
enforceInsideMathFunctions | boolean | false | Lints calc()/clamp()/min()/max() internals |
mathFunctionArguments | Record<mathFn, number[]> | {} | Restricts linting to specific 1-based argument indexes per math function |
ignoreMathFunctionArguments | Record<mathFn, number[]> | {} | Excludes specific 1-based argument indexes per math function |
propertyGroups | Array<'spacing' | 'radius' | 'typography' | 'size' | 'motion'> | ['spacing'] | Selects built-in property groups when properties is not provided |
properties | `Array<string | RegExp>` | built-in spacing patterns |
propertyScales | Record<propertyOrRegex, scaleOrPreset> | {} | Per-property scale overrides (supports exact names or /regex/flags keys; stateful g/y flags are normalized for deterministic matching) |
rhythmguard/prefer-tokenEnforces token usage for spacing declarations. This is ideal once your token system is stable.
Example:
/* ❌ Raw literals */
.stack {
gap: 12px;
padding: 16px;
}
/* ✅ Tokenized */
.stack {
gap: var(--space-3);
padding: var(--space-4);
}
Options:
| Option | Type | Default | Description |
|---|---|---|---|
tokenPattern | string | ^--space- | Regex for accepted token variable names |
tokenFunctions | string[] | ['var','theme','token'] | Functions treated as tokenized values |
allowNumericScale | boolean | false | Temporary migration mode to permit on-scale literals |
preset | string | rhythmic-4 | Selects a built-in scale used in migration mode |
customScale | `Array<number | string>` | undefined |
scale | `Array<number | string>` | [0,4,8,12,16,24,32,40,48,64] |
baseFontSize | number | 16 | Used for scale checks with rem/em |
unitStrategy | 'convert' | 'exact' | 'convert' | Matching strategy when allowNumericScale is enabled |
units | string[] | ['px','rem','em'] | Units considered for numeric scale checks |
enforceInsideMathFunctions | boolean | false | Lints calc()/clamp()/min()/max() internals |
mathFunctionArguments | Record<mathFn, number[]> | {} | Restricts linting to specific 1-based argument indexes per math function |
ignoreMathFunctionArguments | Record<mathFn, number[]> | {} | Excludes specific 1-based argument indexes per math function |
tokenMap | Record<string,string> | {} | Enables autofix from raw value to token |
tokenMapFile | string | null | JSON file path to merge additional token mappings (supports flat, Style Dictionary, and W3C DTCG formats) |
tokenMapFromCssCustomProperties | boolean | false | Auto-builds mappings from matching custom property declarations in the same stylesheet |
tokenMapFromTailwindSpacing | boolean | false | Auto-builds mappings from theme.spacing and theme.extend.spacing in Tailwind config |
tailwindConfigPath | string | null | Path to Tailwind config used by tokenMapFromTailwindSpacing (.js, .cjs, .mjs) |
ignoreValues | string[] | CSS global keywords + auto | Skips keyword literals |
propertyGroups | Array<'spacing' | 'radius' | 'typography' | 'size'> | ['spacing'] | Selects built-in property groups when properties is not provided |
properties | `Array<string | RegExp>` | built-in spacing patterns |
propertyScales | Record<propertyOrRegex, scaleOrPreset> | {} | Per-property scale overrides for numeric migration mode (stateful g/y flags are normalized for deterministic matching) |
rhythmguard/no-offscale-transformSpecialized guardrail for motion spacing consistency in translation transforms.
Example:
/* ❌ Off-scale motion */
.toast {
transform: translateY(18px) scale(1);
}
/* ✅ Motion on spacing scale */
.toast {
transform: translateY(16px) scale(1);
}
Options:
rhythmguard/no-offscale-transform accepts the same scale options as rhythmguard/use-scale (including unitStrategy, math argument targeting, and deterministic autofix), but only for transform translation properties. Its secondary options are also validated for unknown keys and invalid value shapes.
rhythmguard/use-motion-scaleOpt-in guardrail for duration, delay, and easing rhythm.
Example:
/* ❌ Off-scale timing + raw easing */
.button {
transition: opacity 175ms cubic-bezier(.2, 0, 0, 1);
}
/* ✅ Timing on motion scale */
.button {
transition: opacity 150ms var(--ease-snappy);
}
Options:
| Option | Type | Default | Description |
|---|---|---|---|
durationScale | number[] | [0,75,100,150,200,300,500,700,1000] | Allowed duration and delay values in milliseconds |
durationUnits | Array<'ms' | 's'> | ['ms','s'] | Time units considered by the rule |
fixToScale | boolean | true | Autofixes simple duration/delay values to the nearest scale value |
easingTokenMap | Record<string,string> | {} | Optional exact replacements for raw easing functions |
Tailwind class strings can use the ESLint companion rule rhythmguard-tailwind/tailwind-class-use-motion-scale for duration-[...], delay-[...], and ease-[...] arbitrary values.
Rhythmguard works well in Tailwind projects, but it enforces what Stylelint can parse: CSS declarations.
globals.css, components.css, utilities.css*.module.css)@layer blocksThe tailwind config preset automatically extracts spacing tokens from Tailwind v4 @theme blocks and uses them for prefer-token enforcement. Raw values like padding: 16px are autofixed to padding: var(--spacing-4).
See docs/TAILWIND.md for full setup.
class="p-4 gap-2"class="p-[13px] translate-y-[18px]"Those are not Stylelint declaration nodes, so they are outside Stylelint rule scope. Use the ESLint companion rule below for scale-aware class-string enforcement.
Rhythmguard now ships an ESLint companion export for class-string governance:
// eslint.config.js (flat config)
import rhythmguard from 'stylelint-plugin-rhythmguard/eslint';
export default [
{
plugins: {
'rhythmguard-tailwind': rhythmguard,
},
rules: {
'rhythmguard-tailwind/tailwind-class-use-scale': ['error', { scale: [0, 4, 8, 12, 16, 24, 32] }],
},
},
];
This rule targets arbitrary spacing utilities such as p-[13px], gap-[18px], translate-x-[10px], and autofixes to the nearest configured scale value.
The rule checks every string literal in your code, so it works automatically with common utility functions:
cn("p-[13px]") / cn("p-[13px]", condition && "m-[7px]")clsx("p-[13px]", "gap-[18px]")twMerge("p-[13px]", otherClasses)cva("base", { variants: { size: { sm: "p-[5px]" } } })<div className={cn("p-[13px]")} />No extra config needed — if the string contains an arbitrary spacing value, it gets caught and autofixed.
Use both layers:
Suggested setup:
{
"extends": ["stylelint-plugin-rhythmguard/configs/tailwind"]
}
Then pair with:
stylelint-plugin-rhythmguard/eslint for arbitrary spacing class-string scale enforcement.eslint-plugin-tailwindcss for broader class-string linting and conventions. If your policy is to ban every arbitrary value, enable its tailwindcss/no-arbitrary-value rule; use Rhythmguard when you want spacing-specific scale checks and nearest-value fixes.prettier-plugin-tailwindcss for deterministic class ordering.Detailed setup reference: docs/TAILWIND.md.
By default, tokenFunctions includes theme, so values like theme(spacing.4) are treated as tokenized values.
This keeps CSS declaration enforcement and template class-string enforcement separated but coordinated.
const rhythmguard = require('stylelint-plugin-rhythmguard');
console.log(rhythmguard.presets.listScalePresetNames());
console.log(rhythmguard.presets.listCommunityScalePresetNames());
console.log(rhythmguard.presets.getCommunityScaleMetadata('product-decimal-10'));
console.log(rhythmguard.presets.scales['rhythmic-4']);
console.log(Object.keys(rhythmguard.eslint.rules));
The tokenMapFile option supports multiple JSON formats:
Flat token-to-value:
{ "--spacing-4": "16px", "--spacing-3": "12px" }
Style Dictionary:
{ "--spacing-4": { "value": "16px" } }
W3C DTCG (Design Token Community Group):
{
"spacing": {
"4": { "$value": "16px", "$type": "dimension" },
"2": { "$value": "8px", "$type": "dimension" }
}
}
Nested DTCG groups are walked recursively. The key path becomes the CSS variable name: spacing.4 → var(--spacing-4). Non-length values (colors, fonts) are ignored automatically.
Rhythmguard only applies deterministic fixes:
tokenMap replacements for token migrationIt will not guess token mappings without your map.
^16.0.0 || ^17.0.0>=18.18.0require + import entry points (CommonJS + ESM wrappers)16.0.0 has known autofix/API behavior differences; CI enforces floor compatibility and runs non-blocking full-suite observability on the floor version.npm install
npm run lint
npm test
npm run test:coverage
Compare runtime against stylelint-scales on a deterministic spacing corpus:
npm run bench:perf
Benchmark with autofix enabled:
npm run bench:perf:fix
Detailed methodology and custom args are documented in docs/BENCHMARKING.md.
Public codebases currently used for production migration examples:
Want your team listed here?
used-by in the title.release.yml runs the Node/Stylelint matrix validation.NPM_TOKEN is configured in repository secrets, the package is published to npm with provenance (npm publish --provenance).NPM_TOKEN is not configured, publish is skipped with an explicit workflow notice.post-publish-smoke.yml verifies the published npm version can be installed and run in a clean project (and skips cleanly if the version is not on npm).hello@petrilahdelma.comMIT. See LICENSE.
FAQs
Token governance for CSS and Tailwind — enforce spacing scales, require design tokens, catch arbitrary values
The npm package stylelint-plugin-rhythmguard receives a total of 318 weekly downloads. As such, stylelint-plugin-rhythmguard popularity was classified as not popular.
We found that stylelint-plugin-rhythmguard demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
Open VSX has removed three extension IDs from its malicious-extension list as the legitimate publishers they impersonated move to claim the names for themselves.

Product
Socket’s PHP and Composer support is now in Beta for all customers, with PHP reachability analysis generally available.

Product
Socket is bringing experimental protection to Firefox, scanning 97,000+ extensions in Mozilla's official directory for malware and risky updates.