Spacing Utilities Without the Utility CSS Explosion
Generating padding and margin helper classes with a Sass loop is a common pattern, but it produces a surprising amount of CSS. A simple implementation with 11 spacing values generates roughly 8.6kb of uncompressed CSS (under 1kb compressed with Brotli or gzip). That’s already substantial for what are essentially repetitive declarations. Add responsive breakpoints into the mix, and the output balloons to about 41.7kb uncompressed.
There’s a leaner approach: assign a CSS custom property inline on the element, then use calc() in the stylesheet to turn that numeric value into a real spacing measurement. This eliminates the need to pre-generate every possible class at build time.
Inline Custom Properties as a Spacing Scale
Instead of class="p-4", the element carries its own variable:
<div style="--p: 4;"></div>
The stylesheet then converts that integer into a rem value using a scale factor:
[style*="--p:"] { padding: calc(var(--p) * 0.25rem) !important; }
In this example, --p: 4 resolves to padding: 1rem !important;. The key insight is that the CSS rule doesn’t need to know which value is coming; the calculation happens at render time, so the system is effectively infinite. No pre-defined list of classes is required.
The full spacer utility set, covering padding and margin on all four sides, comes to roughly 1.4kb uncompressed (226 bytes with Brotli, 284 bytes with gzip). Each property needs just one declaration with the appropriate calc() expression:
[style*="--p:"] { padding: calc(var(--p) * 0.25rem) !important; }
[style*="--pt:"] { padding-top: calc(var(--pt) * 0.25rem) !important; }
[style*="--pr:"] { padding-right: calc(var(--pr) * 0.25rem) !important; }
[style*="--pb:"] { padding-bottom: calc(var(--pb) * 0.25rem) !important; }
[style*="--pl:"] { padding-left: calc(var(--pl) * 0.25rem) !important; }
[style*="--m:"] { margin: calc(var(--m) * 0.25rem) !important; }
/* ...and so on for each side */
Handling Responsive Breakpoints
Extending this pattern to breakpoints requires a naming strategy, since the @ character isn’t allowed in CSS variable names. Using a suffix like sm or md works fine, though:
<div style="--p-sm: 2; --p-lg: 4;"></div>
The stylesheet adds media query–scoped rules for each breakpoint variable:
@media (min-width: 640px) {
[style*="--p-sm:"] { padding: calc(var(--p-sm) * 0.25rem) !important; }
}
@media (min-width: 1024px) {
[style*="--p-lg:"] { padding: calc(var(--p-lg) * 0.25rem) !important; }
}
This responsive version clocks in at about 6.1kb uncompressed (428 bytes Brotli, 563 bytes gzip). That’s a substantial reduction from the 41.7kb the equivalent class-based Sass output produces, and the number of breakpoints doesn’t exponentially multiply the output the way a build-time loop does.
Considerations and Limitations
This technique has a few caveats worth noting:
- Developer ergonomics: Writing
<div style="--px:2; --my:4;">is verbose and less readable than a utility class for anyone maintaining the markup. - Scoping is safe: Inline CSS custom properties are scoped to the element they’re set on; they don’t leak to descendants or affect the global variable value.
- DevTools gap: Current versions of Chrome, Firefox, and Safari do not show these derived values in the “Computed” styles panel, which complicates debugging.
- Not for
attr(): The initial idea of usingcalc(attr(data-m) * 0.25rem)is currently impossible;attr()remains limited to thecontentproperty.
Broader Applications
The approach suits properties on a linear, incremental scale — spacing, grid column widths, or heights. Typographic scales are a possible but less natural fit. The technique could also be a strategy for critical CSS minifiers to reduce output, even if hand-authoring styles this way isn’t appealing. For truly size-constrained contexts, it’s possible to shave further by selectively using logical properties (padding-block, padding-inline) alongside traditional ones, getting the full spacer set down to about 400 bytes with Brotli.



