CSS Spacing System: 8px Grid, rem vs px, and Container Queries
Master the 8px spacing grid, know when to use rem over px, and write container queries that actually hold up in production layouts.
Why Your Spacing Feels Off (And It's Probably Not the Font)
Most spacing issues in UI aren't caused by bad taste. They're caused by an inconsistent scale — some margins are 12px, some are 15px, one padding is 20px and another is 24px, and nothing lines up visually. Layouts that feel 'almost right' are almost always the result of ad-hoc spacing rather than a deliberate system.
The fix isn't complicated. Pick a base unit and stick to it everywhere. That's the core idea behind the 8px grid — every spacing value you use is a multiple of 8: 8, 16, 24, 32, 40, 48, and so on. It works because most screen resolutions divide evenly by 8, which means pixel-perfect rendering without subpixel blurring on common device densities.
In practice, you won't always need strict multiples. A 4px gap for tight inline spacing is totally valid — 4 is half of 8, so it still fits the system. What you're avoiding is the random 13px padding or the 22px margin that someone typed in freehand. Those are the values that break visual rhythm.
Building the 8px Token Scale in CSS
The cleanest implementation is a set of CSS custom properties you define once and reference everywhere. No magic numbers, no mental arithmetic at 11pm.
:root {
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-10: 2.5rem; /* 40px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
}
.card {
padding: var(--space-6);
gap: var(--space-4);
margin-bottom: var(--space-8);
}Worth noting: I'm using rem here, not px — that matters, and we'll get into why in the next section. The naming convention (--space-4 for 1rem/16px) maps directly to Tailwind's scale if your team is already used to thinking in those terms. That alignment reduces cognitive overhead when you're switching between utility classes and raw CSS.
One more thing — this scale doesn't need to be exhaustive on day one. Start with 8 or 9 values, and add more only when a real design need appears. A bloated token system you don't actually use is worse than a minimal one you do.
rem vs px: The Real Answer
Here's the argument you see constantly: 'Use rem so users can resize text.' Here's why that's only half the story.
rem is relative to the root font size — typically 16px unless the user or browser overrides it. When a user bumps their browser font size to 20px, a value of 1rem becomes 20px instead of 16px. That means your spacing also scales up proportionally. For components tied to typography — card padding, button height, form field gaps — that's exactly what you want. Content and its surrounding space grow together.
Honestly, the cases where you'd stick with px are fewer than people think. Fixed-size decorative elements (a 1px border, a 2px divider line, a 4px border-radius on a chip) don't need to scale with font size. That's it. Everything else — margins, padding, gap, layout spacing — should be in rem.
Quick aside: em isn't the same as rem. em is relative to the *current* element's font size, which means nesting compounds it unpredictably. You almost never want em for spacing. If you're using it for line-height inside a text component, that's fine — but for layout spacing, always reach for rem.
Container Queries: Spacing That Responds to the Component, Not the Viewport
Media queries have been the go-to for responsive layout since 2012. They work. But they break down the moment you start reusing components in different contexts — a card that's in a full-width section on one page and a 300px sidebar on another. The component doesn't know where it is, so viewport-based breakpoints become guesswork.
Container queries landed in baseline browsers in 2023 and they change the equation completely. You define a containment context on the parent, then query the parent's size from within the child. The component responds to its *actual available space*, not the viewport.
.card-wrapper {
container-type: inline-size;
container-name: card;
}
.card {
padding: var(--space-4);
display: grid;
grid-template-columns: 1fr;
gap: var(--space-4);
}
@container card (min-width: 480px) {
.card {
padding: var(--space-6);
grid-template-columns: auto 1fr;
gap: var(--space-6);
}
}
@container card (min-width: 720px) {
.card {
padding: var(--space-8);
}
}That 480px threshold isn't arbitrary — it's a multiple of 8 times 60, which maps to a common column width. Keeping your breakpoint numbers tethered to the same base unit as your spacing reduces the number of arbitrary values in your codebase.
Look, container queries won't replace media queries entirely. Page-level layout decisions — the sidebar showing or hiding, the nav collapsing — those still belong on the viewport. But for any reusable component, container queries are the better tool now. If you're building a design system in 2026, there's no good reason to ship components that only know about viewport width.
Applying the System to Real Components
Theory is fine, but let's talk about what happens when you actually wire this up. If you're working with something like Empire UI — which ships components across multiple visual styles — a consistent spacing token layer underneath all the style variations is what keeps things coherent. The glassmorphism card and the neobrutalism card can look completely different and still share the same --space-6 padding.
The pattern that works in practice: define your spacing tokens at the design-system level (the :root block), let components consume them via var(), and override at the theme level if a specific style needs tighter or looser density. The glassmorphism components on Empire UI follow this exactly — the blur and transparency effects are theme-layer concerns, but the padding scale stays consistent.
One failure mode to watch for: teams that define tokens but then hardcode px values in component files anyway. It happens during fast feature sprints. A code review rule or a lint check (no-magic-numbers in Stylelint) will catch it before it becomes a habit.
That said, don't over-engineer the token layer for a small project. If you're building a personal site or a prototype, --space-4 through --space-12 is plenty. Reach for the full scale only when you have multiple components and multiple people touching the CSS.
Spacing and CSS Grid: They're Not Separate Problems
A spacing system doesn't exist in isolation from your grid system — they're the same problem. The column gutters in your grid layout should come from the same token scale as your component padding. If your grid uses a 24px gutter (--space-6) and your cards use --space-4 internal padding, the visual weight is inconsistent. Not wrong exactly, but off.
The tools like the gradient generator and box shadow generator on Empire UI are good examples of single-purpose UIs where the layout is tight and deliberate — those pages use consistent 8px-based spacing to keep controls visually grouped without eating screen real estate.
For a full page layout, a pattern that holds up well is: outer container uses --space-8 to --space-16 for section padding, inner grid uses --space-6 for column gap, and individual components use --space-4 to --space-6 for internal padding. Three levels of spacing density that correspond to three levels of visual hierarchy. Simple, memorable, and easy to enforce in a team setting.
Common Mistakes and How to Avoid Them
The most common mistake is mixing systems. You start with the 8px grid, then a designer drops a comp with 18px padding somewhere, and someone codes it as padding: 18px because it matched the mockup pixel-for-pixel. Now you've got an exception in the system. The fix isn't to argue about the 18px — it's to round to 16px (--space-4) or 20px (not on the 8px grid, but 4px is fine as a half-step) and flag it in the design review before it lands in code.
Another one: using margin where gap would work. In a flex or grid context, gap is predictable and doesn't collapse. margin between grid children still works, but gap is cleaner and doesn't require you to remove the last child's margin. In 2026, if your codebase still has .card + .card { margin-top: 24px; } everywhere, that's technical debt worth paying down.
Finally — don't forget to test your container queries at the actual container width, not the viewport width. Browser devtools added container size overlays in Chrome 105 and Firefox 110. Use them. If you're eyeballing the behavior by resizing the browser window, you're testing the wrong variable.
FAQ
Use rem for almost everything — padding, margin, gap — so spacing scales with the user's font size preference. Reserve px for fixed decorative elements like 1px borders or specific border-radius values that shouldn't scale.
It's a convention where all spacing values are multiples of 8px (or 4px as a half-step). It works because most display densities divide evenly by 8, which avoids subpixel rendering artifacts and keeps visual rhythm consistent across a layout.
Yes. Container queries have been in Baseline since 2023 and are supported across Chrome, Firefox, and Safari. If you need to support browsers from before mid-2023, add a feature query fallback — but for most modern projects you're fine.
Media queries respond to the viewport width; container queries respond to the parent element's width. For reusable components that appear in different layout contexts, container queries give you accurate, context-aware spacing without hacks.