Enforce a Project-Wide Spacing Scale Across .astro Components and Islands Through One Modifier Pattern
Custom Modifier Scale in Astro
Design systems lose to drift: one engineer writes padding: "14px", another writes padding: "0.875rem", and the spacing scale a designer carefully built starts breaking apart. Salty CSS lets you build a proprietary DSL for your design system with defineConfig modifiers. A modifier is a regex pattern plus a transform function — when Salty CSS sees a matching value at build time, it rewrites it. The pattern is yours to design; the enforcement is automatic.
Register the modifier
// /salty.config.ts import { defineConfig } from "@salty-css/core/config"; export const config = defineConfig({ modifiers: { // Shorthand: write `space:3` and get `12px` (3 × 4px base unit). spaceShorthand: { pattern: /^space:(\d+)$/, transform: (match) => { const n = Number(match.replace("space:", "")); return { value: `${n * 4}px` }; }, }, // Inject extra CSS alongside the rewritten value: elevation: { pattern: /^elevation:(\d+)$/, transform: (match) => { const level = Number(match.replace("elevation:", "")); return { value: `${level * 2}px ${level * 4}px ${level * 6}px rgba(0,0,0,0.12)`, css: { transform: "translateZ(0)" }, // emitted alongside }; }, }, }, });
Use the shorthand at the call site:
styled("div", { base: { padding: "space:3", // → 12px boxShadow: "elevation:2", // → 4px 8px 12px rgba(0,0,0,0.12) }, });
spaceShorthand matches strings like "space:3" and rewrites them to "12px". The transform receives the full matched string (not the capture groups) — re-parse it inside the function to extract the numeric portion.
Use the DSL in any Astro component
// /components/card.css.ts import { styled } from "@salty-css/astro/styled"; export const Card = styled("article", { base: { // Reads as "padding step 4 on the design scale" — the regex // ensures only valid scale steps compile, off-scale values fail loudly. padding: "space:4", gap: "space:2", borderRadius: "space:1", }, });
Why this beats the obvious alternatives
- A
paddingvariant for every scale step explodes the variant API and ties spacing to component identity. - Design tokens via
defineVariablesare great for values, but they don't surface as a fluent shorthand at the call site. A modifier is intentionally regex-driven, so unmatched values fall back to plain CSS without the developer having to think about it. - Code review as the enforcement layer is human, slow, and inconsistent. A modifier moves the check to build time.
What it produces
The matched string is replaced with the transform's return value before the parser sees it. After the build, padding: "space:4" is padding: 16px in saltygen/index.css — no trace of the DSL is left in the bundle. Off-scale values don't match the pattern and pass through unchanged; if you want hard enforcement you can throw inside the transform on illegal inputs, and the build will fail.
Gotchas
- Anchor the pattern.
/^space:(\d+)$/is right;/space:(\d+)/will match"space:3 extra"and break with surprising results. The^…$anchors keep the rewrite explicit. - Strings only. Modifiers run against string values.
padding: 12(numeric) is handled bydefaultUnitondefineConfig, not by modifiers. - One scale, one source of truth. If your modifier is "multiply by 4", document that. Future readers won't reverse-engineer the curve from the transform.
- Inject extra CSS sparingly. A modifier can return
{ value, css }to emit additional declarations next to the rewritten property (the snippet above includes anelevationexample). It's powerful for shadows and gradients, but easy to abuse.
See also
- Modifiers reference — pattern, transform,
cssside-effects, and when to reach for tokens or templates instead. defineConfigreference — where modifiers live in the config.