Spacing systems fail when every component invents its own breakpoint. The result is a page where sections grow at different widths for reasons nobody can explain.
The better approach is to define spacing relationships and let components opt into a small set of page rhythms. Responsive spacing should protect hierarchy, readability, and component stability. It should not simply get larger because the viewport got wider.
Use tokens for rhythm, not decoration
:root {
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
}
A spacing scale is not valuable because it has many values. It is valuable because it limits arguments. If a card body uses --space-4 and a section uses --space-6, the relationship is visible.
Use custom properties for spacing roles that need to vary by context.
.stack {
display: grid;
gap: var(--stack-space, var(--space-4));
}
.sidebar .stack {
--stack-space: var(--space-3);
}
The component does not need a modifier class just to become denser inside a sidebar. The context changes the spacing decision.
Change rhythm at layout boundaries
Instead of writing custom media queries inside every component, adjust section or layout variables at the boundary.
.page-section {
--section-space: clamp(3rem, 8vw, 6rem);
padding-block: var(--section-space);
}
.page-section[data-density="compact"] {
--section-space: clamp(2rem, 5vw, 4rem);
}
Components inside the section can use local spacing without knowing about the viewport. The section decides its rhythm. Cards decide their internal rhythm. The page shell decides its gutters.
Use clamp() where continuous change helps
clamp() is useful when a value should scale between a minimum and maximum.
:root {
--page-gutter: clamp(1rem, 4vw, 3rem);
--section-space: clamp(3rem, 8vw, 6rem);
}
.page-section__inner {
width: min(100% - (var(--page-gutter) * 2), 72rem);
margin-inline: auto;
}
The minimum protects small screens. The middle value lets the spacing breathe. The maximum prevents oversized gaps on wide displays. Use this for page gutters, editorial spacing, hero rhythm, and large display type. Avoid it where stable dimensions matter.
Avoid fluid values where stability matters
Fluid spacing can be useful for editorial pages, but fixed-format UI elements need stable dimensions. Toolbars, icon buttons, board cells, counters, and dense dashboards often behave better with defined sizes and media-query steps.
.icon-button {
inline-size: 2.5rem;
block-size: 2.5rem;
}
.toolbar {
gap: 0.5rem;
}
A toolbar that changes gap continuously may cause labels to wrap at awkward widths. A board cell that scales with viewport width may become too small to tap or too large to scan. The goal is not to make every value fluid. The goal is to prevent layout jumps and overlap.
Test with real content
Spacing that looks good with short labels can fail with localization, long product names, code snippets, and missing images. Test spacing against:
- A card with a long heading.
- A card with missing media.
- A form label that wraps.
- A table or code block inside the main column.
- A narrow sidebar.
- A zoomed browser.
Responsive spacing is a content problem as much as a viewport problem. Good responsive spacing is quiet: it lets content breathe without making the CSS feel like a pile of unrelated breakpoints.
Keep local density local
Do not make every component listen directly to the viewport just because it needs a compact mode. A card inside a sidebar may need tighter spacing on a wide desktop, while the same card in the main column can stay relaxed. Let the container set a local spacing role:
.card {
display: grid;
gap: var(--card-gap, var(--space-4));
padding: var(--card-padding, var(--space-5));
}
.sidebar {
--card-gap: var(--space-3);
--card-padding: var(--space-4);
}
That approach keeps the component reusable. It also makes density an explicit context decision instead of another global breakpoint branch.
Use fluid spacing sparingly inside repeated UI. A list of cards with continuously changing gaps can feel unstable as the viewport changes. Stable internal spacing and fluid page gutters are often a better combination: the page breathes, while repeated components remain easy to scan.
When a value needs a breakpoint, make that breakpoint belong to the layout boundary that owns the change. A documentation article might increase section spacing at a wide reading measure. A toolbar might keep fixed gaps until it wraps. A pricing grid might switch from stacked to columns based on container space. Those are separate decisions and should not all depend on the same global width token.
Keep notes for unusual values. If a spacing value exists to protect a line length, reserve space for a sticky header, or align with an image ratio, write that reason near the rule. Future maintainers are less likely to replace a meaningful constraint with a prettier number.
Related guides: Custom properties, Container queries, and Logical properties.