Phase
Phase 1 — Complete the primitive tier
Prerequisite
Blocked by #287 (tier-first directory restructure) only — this issue is independent of the color-family work in #301/#302/#303.
Concern
Expand tokens/system/spacing/spacing.json from its current partial set (multiples 05–6 only) to the full computed USWDS spacing scale, and annotate every computed entry with $extensions.uswds.formula for CI drift-guard recomputation.
The full scale covers:
- Multiples:
05, 1, 105, 2, 205, 3, 4, 5, 6, 7, 8, 9, 10, 15 (computed as grid-base × m, where grid-base = 8px = 0.5rem and m is the token's fractional multiple, not its key digits — the keys are shorthand: 05 → 0.5, 105 → 1.5, 205 → 2.5, 1 → 1, 2 → 2, etc., matching USWDS core's spacing-multiple(m))
- Named aliases:
card (10rem), card-lg (15rem), mobile (20rem), mobile-lg (30rem), tablet (40rem), tablet-lg (55rem), desktop (64rem), desktop-lg (75rem), widescreen (87.5rem)
- Negatives (
neg-*): negative forms of multiples 05–15 and 1px/2px
- Pixel literals:
1px, 2px (not formula-derived; 0 and auto are not tokens — they are CSS keywords)
All current entries in spacing.json gain $extensions.uswds.formula retroactively. A new CI validation script recomputes and verifies formula-tagged values against the 8px grid constant.
Files touched
- Modify
tokens/system/spacing/spacing.json — full scale, negatives, named, formulas
- New
internals/scripts/validate-spacing-formulas.js — recompute + diff CI script
- Modify
package.json — add "validate:spacing" script
- Rebuild
build/css/system/spacing.css, build/scss/system/_spacing.scss
Implementation steps
1. Extend tokens/system/spacing/spacing.json
Full scale structure (excerpt — all entries follow this pattern):
{
"spacing": {
"$type": "dimension",
"grid-base": {
"$value": { "value": 0.5, "unit": "rem" },
"$description": "8px base unit (8px ÷ 16px = 0.5rem). All multiples derive from this.",
"$extensions": {
"uswds": {
"tier": "system",
"formula": "8px / root-font-size"
}
}
},
"05": {
"$value": { "value": 0.25, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 0.5",
"legacyName": {
"publicVar": "$system-spacing-small-05",
"mapKey": "05"
}
}
}
},
"1": {
"$value": { "value": 0.5, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 1",
"legacyName": {
"publicVar": "$system-spacing-small-1",
"mapKey": "1"
}
}
}
},
"105": {
"$value": { "value": 0.75, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 1.5",
"legacyName": {
"publicVar": "$system-spacing-small-105",
"mapKey": "105"
}
}
}
},
"neg-05": {
"$value": { "value": -0.25, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * -0.5",
"legacyName": {
"publicVar": "$system-spacing-small-negative-neg-05"
}
}
}
},
"1px": {
"$value": { "value": 1, "unit": "px" },
"$extensions": {
"uswds": {
"tier": "system",
"legacyName": {
"publicVar": "$system-spacing-smaller-1px"
}
}
}
},
"card": {
"$value": { "value": 10, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 20",
"legacyName": {
"publicVar": "$system-spacing-large-card",
"mapKey": "card"
}
}
}
},
"card-lg": {
"$value": { "value": 15, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 30",
"legacyName": {
"publicVar": "$system-spacing-large-card-lg",
"mapKey": "card-lg"
}
}
}
},
"mobile": {
"$value": { "value": 20, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 40",
"legacyName": {
"publicVar": "$system-spacing-large-mobile",
"mapKey": "mobile"
}
}
}
},
"mobile-lg": {
"$value": { "value": 30, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 60",
"legacyName": {
"publicVar": "$system-spacing-larger-mobile-lg",
"mapKey": "mobile-lg"
}
}
}
},
"tablet": {
"$value": { "value": 40, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 80",
"legacyName": {
"publicVar": "$system-spacing-larger-tablet",
"mapKey": "tablet"
}
}
}
},
"tablet-lg": {
"$value": { "value": 55, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 110",
"legacyName": {
"publicVar": "$system-spacing-larger-tablet-lg",
"mapKey": "tablet-lg"
}
}
}
},
"desktop": {
"$value": { "value": 64, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 128",
"legacyName": {
"publicVar": "$system-spacing-largest-desktop",
"mapKey": "desktop"
}
}
}
},
"desktop-lg": {
"$value": { "value": 75, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 150",
"legacyName": {
"publicVar": "$system-spacing-largest-desktop-lg",
"mapKey": "desktop-lg"
}
}
}
},
"widescreen": {
"$value": { "value": 87.5, "unit": "rem" },
"$extensions": {
"uswds": {
"tier": "system",
"formula": "grid-base * 175",
"legacyName": {
"publicVar": "$system-spacing-largest-widescreen",
"mapKey": "widescreen"
}
}
}
}
}
}
Named spacing tokens (card through widescreen) are literal spacing-multiple(m) formula entries, not aliases of the small-scale numeric multiples. Each value equals grid-base × m = m × 0.5rem, matching USWDS core's spacing.scss exactly. Note the multiplier m is the value passed to spacing-multiple() in core, which for the fractional keys is not the key digits — e.g. 105 → spacing-multiple(1.5), 205 → spacing-multiple(2.5):
| token |
spacing-multiple(m) |
value |
card |
spacing-multiple(20) |
10rem |
card-lg |
spacing-multiple(30) |
15rem |
mobile |
spacing-multiple(40) |
20rem |
mobile-lg |
spacing-multiple(60) |
30rem |
tablet |
spacing-multiple(80) |
40rem |
tablet-lg |
spacing-multiple(110) |
55rem |
desktop |
spacing-multiple(128) |
64rem |
desktop-lg |
spacing-multiple(150) |
75rem |
widescreen |
spacing-multiple(175) |
87.5rem |
These values also need to match the current tokens/system/breakpoints/breakpoints.json exactly — the breakpoint-aliasing issue's gate ("same resolved values as before") depends on it. A CI validate-spacing script (below) recomputes all formula-tagged values from grid-base = 0.5rem.
Also add the remaining multiples not shown above (2, 205, 3, 4, 5, 6, 7, 8, 9, 10, 15) and their neg-* counterparts (neg-1 through neg-15, neg-1px, neg-2px) following the same grid-base * m formula pattern.
2. validate-spacing-formulas.js
// Reads tokens/system/spacing/spacing.json
// For each entry with $extensions.uswds.formula:
// - parses the multiplier from the formula STRING ("grid-base * m"),
// NOT from the token key — keys like "105"/"205" are shorthand whose
// real multiplier is 1.5/2.5, and the correct value lives in the formula
// - evaluates "grid-base * m" using a fixed GRID_BASE = 0.5rem
// - compares to the token's $value using an epsilon tolerance, NOT strict equality:
// const EPSILON = 1e-5;
// const isMatch = Math.abs(computed - tokenValue) < EPSILON;
// (plain `===` false-fails on IEEE-754 rounding, e.g. 0.5 * 0.5 vs 0.25)
// - exits non-zero and prints a diff table on any mismatch
Add to package.json:
"validate:spacing": "node internals/scripts/validate-spacing-formulas.js"
3. Update config/style-dictionary.config.js
Ensure the disabled-filter and tier-segment-drop from #287's restructure also apply to the spacing platform file.
4. Run build and validation
npm run build:tokens
node internals/scripts/validate-spacing-formulas.js
Done when
Phase
Phase 1 — Complete the primitive tier
Prerequisite
Blocked by #287 (tier-first directory restructure) only — this issue is independent of the color-family work in #301/#302/#303.
Concern
Expand
tokens/system/spacing/spacing.jsonfrom its current partial set (multiples05–6only) to the full computed USWDS spacing scale, and annotate every computed entry with$extensions.uswds.formulafor CI drift-guard recomputation.The full scale covers:
05,1,105,2,205,3,4,5,6,7,8,9,10,15(computed asgrid-base × m, where grid-base =8px=0.5remandmis the token's fractional multiple, not its key digits — the keys are shorthand:05→0.5,105→1.5,205→2.5,1→1,2→2, etc., matching USWDS core'sspacing-multiple(m))card(10rem),card-lg(15rem),mobile(20rem),mobile-lg(30rem),tablet(40rem),tablet-lg(55rem),desktop(64rem),desktop-lg(75rem),widescreen(87.5rem)neg-*): negative forms of multiples 05–15 and1px/2px1px,2px(not formula-derived;0andautoare not tokens — they are CSS keywords)All current entries in
spacing.jsongain$extensions.uswds.formularetroactively. A new CI validation script recomputes and verifies formula-tagged values against the 8px grid constant.Files touched
tokens/system/spacing/spacing.json— full scale, negatives, named, formulasinternals/scripts/validate-spacing-formulas.js— recompute + diff CI scriptpackage.json— add"validate:spacing"scriptbuild/css/system/spacing.css,build/scss/system/_spacing.scssImplementation steps
1. Extend
tokens/system/spacing/spacing.jsonFull scale structure (excerpt — all entries follow this pattern):
{ "spacing": { "$type": "dimension", "grid-base": { "$value": { "value": 0.5, "unit": "rem" }, "$description": "8px base unit (8px ÷ 16px = 0.5rem). All multiples derive from this.", "$extensions": { "uswds": { "tier": "system", "formula": "8px / root-font-size" } } }, "05": { "$value": { "value": 0.25, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 0.5", "legacyName": { "publicVar": "$system-spacing-small-05", "mapKey": "05" } } } }, "1": { "$value": { "value": 0.5, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 1", "legacyName": { "publicVar": "$system-spacing-small-1", "mapKey": "1" } } } }, "105": { "$value": { "value": 0.75, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 1.5", "legacyName": { "publicVar": "$system-spacing-small-105", "mapKey": "105" } } } }, "neg-05": { "$value": { "value": -0.25, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * -0.5", "legacyName": { "publicVar": "$system-spacing-small-negative-neg-05" } } } }, "1px": { "$value": { "value": 1, "unit": "px" }, "$extensions": { "uswds": { "tier": "system", "legacyName": { "publicVar": "$system-spacing-smaller-1px" } } } }, "card": { "$value": { "value": 10, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 20", "legacyName": { "publicVar": "$system-spacing-large-card", "mapKey": "card" } } } }, "card-lg": { "$value": { "value": 15, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 30", "legacyName": { "publicVar": "$system-spacing-large-card-lg", "mapKey": "card-lg" } } } }, "mobile": { "$value": { "value": 20, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 40", "legacyName": { "publicVar": "$system-spacing-large-mobile", "mapKey": "mobile" } } } }, "mobile-lg": { "$value": { "value": 30, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 60", "legacyName": { "publicVar": "$system-spacing-larger-mobile-lg", "mapKey": "mobile-lg" } } } }, "tablet": { "$value": { "value": 40, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 80", "legacyName": { "publicVar": "$system-spacing-larger-tablet", "mapKey": "tablet" } } } }, "tablet-lg": { "$value": { "value": 55, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 110", "legacyName": { "publicVar": "$system-spacing-larger-tablet-lg", "mapKey": "tablet-lg" } } } }, "desktop": { "$value": { "value": 64, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 128", "legacyName": { "publicVar": "$system-spacing-largest-desktop", "mapKey": "desktop" } } } }, "desktop-lg": { "$value": { "value": 75, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 150", "legacyName": { "publicVar": "$system-spacing-largest-desktop-lg", "mapKey": "desktop-lg" } } } }, "widescreen": { "$value": { "value": 87.5, "unit": "rem" }, "$extensions": { "uswds": { "tier": "system", "formula": "grid-base * 175", "legacyName": { "publicVar": "$system-spacing-largest-widescreen", "mapKey": "widescreen" } } } } } }Named spacing tokens (
cardthroughwidescreen) are literalspacing-multiple(m)formula entries, not aliases of the small-scale numeric multiples. Each value equalsgrid-base × m=m × 0.5rem, matching USWDS core'sspacing.scssexactly. Note the multipliermis the value passed tospacing-multiple()in core, which for the fractional keys is not the key digits — e.g.105→spacing-multiple(1.5),205→spacing-multiple(2.5):spacing-multiple(m)cardspacing-multiple(20)card-lgspacing-multiple(30)mobilespacing-multiple(40)mobile-lgspacing-multiple(60)tabletspacing-multiple(80)tablet-lgspacing-multiple(110)desktopspacing-multiple(128)desktop-lgspacing-multiple(150)widescreenspacing-multiple(175)These values also need to match the current
tokens/system/breakpoints/breakpoints.jsonexactly — the breakpoint-aliasing issue's gate ("same resolved values as before") depends on it. A CI validate-spacing script (below) recomputes allformula-tagged values fromgrid-base = 0.5rem.Also add the remaining multiples not shown above (
2,205,3,4,5,6,7,8,9,10,15) and theirneg-*counterparts (neg-1throughneg-15,neg-1px,neg-2px) following the samegrid-base * mformula pattern.2.
validate-spacing-formulas.jsAdd to
package.json:3. Update
config/style-dictionary.config.jsEnsure the
disabled-filter and tier-segment-drop from #287's restructure also apply to the spacing platform file.4. Run build and validation
Done when
npm run build:tokensexits 0npm testexits 0node internals/scripts/validate-spacing-formulas.jsexits 0 (all formula-tagged values match computed values within 1e-5 epsilon tolerance)05through15) present inbuild/css/system/spacing.cssneg-05throughneg-15,neg-1px,neg-2px) present in built outputcardthroughwidescreen) present in built output1pxand2pxliterals present;0andautoare absent (CSS keywords, not tokens)$extensions.uswds.formulasetbuild/output committed alongside source changes