Skip to content

Spacing & Layout

A UI principle for coding agents. Also covers layout, white space, whitespace, grid, sizing, alignment, and 8 more.

Show all 14 aliases

layout, white space, whitespace, grid, sizing, alignment, inset, padding, gestalt, grouping, proximity, control placement, common region, continuity

Start with Too Much White Space

  • Start with more white space than you think you need
  • It's easier to remove than to add
  • White space creates breathing room and reduces cognitive load

Establish a Spacing and Sizing System

Linear Scale (bad):

  • Don't use: 10px, 20px, 30px, 40px, 50px...
  • Too limiting at small sizes, not enough options at large sizes

Better Approach:

  • More options at small sizes (4px, 8px, 12px, 16px)
  • Bigger jumps at large sizes (32px, 48px, 64px, 96px, 128px)

Recommended Scale:

spacing-1: 4px
spacing-2: 8px
spacing-3: 12px
spacing-4: 16px
spacing-5: 24px
spacing-6: 32px
spacing-7: 48px
spacing-8: 64px
spacing-9: 96px
spacing-10: 128px
spacing-11: 192px
spacing-12: 256px

Sizing Scale:

  • Use same scale for width, height, max-width, margin, padding
  • Maintains consistency across the interface

You Don't Have to Fill the Whole Screen

  • Just because you have space doesn't mean you need to use it
  • Adding unnecessary filler content creates clutter
  • Give content a max-width instead of expanding to fill screen
  • Shrink elements until they start to feel cramped, then add space back

Grids are Overrated

  • Don't force yourself to fill all 12 grid columns
  • Use grids as a guide, not a constraint
  • It's okay to have unequal column sizes
  • Consider sidebar + main content (e.g., 1/3 + 2/3) instead of forcing equal columns

Relative Sizing Doesn't Scale

  • Don't use percentages for element sizes
  • Elements that are large on desktop become too large on mobile
  • Elements that are small on desktop become too small on mobile

Better approach:

  • Use absolute sizes (px, rem)
  • Adjust sizes at breakpoints if needed
  • Same applies to font sizes

Avoid Ambiguous Spacing

  • When spacing between elements is ambiguous, it's unclear what's related
  • Use more spacing between groups than within groups
  • Make relationships clear through spacing

A Nested Corner Subtracts the Padding

The padding between two rounded boxes is spacing, so the inner corner is a spacing decision. When a tile, button, avatar, or input sits inside a padded card, the two curves must share one center or the gap between them reads as uneven.

Inner radius = outer radius − padding.

card radius:    30.8px
card padding:      16px
tile radius:    30.8 − 16 = 14.8px

Picking the next token down the radius scale instead is the common mistake, and it is what makes a nested tile look slightly wrong without the reader knowing why. If the subtraction reaches zero or below, the inner element is square; do not force a positive value back in.

Full treatment, including the CSS-variable form that stays correct when the radius changes: docs/ui/depth-and-surfaces.md, "Concentric Radius for Nested Elements".

Structure Is Perceived Before It Is Read

People do not begin by reading every label. They first infer groups, boundaries, and a likely scan path. Six perceptual cues make that structure legible:

  1. Similarity: elements with the same role should share the same visual treatment. Standardize icon style, control size, label treatment, value format, and repeated spacing. A broken pattern forces the user to decode the element again.
  2. Proximity: keep related items close and unrelated groups farther apart. Labels stay attached to inputs, values to their units, and related actions in one visible block.
  3. Simplicity: remove redundant labels, merge duplicate actions, and minimize one-off layout patterns. Reorganizing clutter still leaves clutter; cut before rearranging.
  4. Alignment: choose a small set of visible anchors and reuse them. Consistent left edges, columns, baselines, and tokenized gaps reduce zig-zag scanning and make the invisible grid predictable.
  5. Common region: use a bounded or subtly tinted surface when spacing alone cannot express a group. Keep regions quiet; a background shift or shallow inset is usually enough. Do not turn every group into a bordered card.
  6. Continuity: create a clear path from the first important item through the rest of the screen. Repeated landmarks and consistent edges act as a handrail. Collapsible sections can preserve the overview while revealing detail on demand.

These cues are a practical application of Gestalt grouping.

Spacing Expresses Relationships

When an interface feels noisy, people usually blame shadows, colors, or fonts. Most of the time the real issue is spacing — the layout has no rhythm, so every component feels like it's arguing for attention.

Internal vs external spacing do different jobs:

  • Internal spacing = space inside a component (padding, icon-to-text gap, label-to-input gap)
  • External spacing = space between components (card-to-card, section-to-section, form group to form group)

When internal and external spacing use the same value, the interface loses structure. Every relationship has the same strength, so the UI stops telling you what's a group and what's a new section.

The rule: tighten within groups, loosen between groups. This one rule fixes more noisy layouts than adding random whitespace.

  • Tighter spacing inside a component
  • Slightly looser spacing between related components
  • Noticeably larger spacing between sections

Surface-appropriate density:

  • Marketing pages afford broader spacing (paced reading, emphasis)
  • Forms need moderate spacing (related fields connected, still scannable)
  • Dense admin tables need tighter spacing (more info visible without losing legibility)
  • Don't force one spacing rhythm across all surfaces

Text-to-control rhythm:

  • Labels should feel attached to their inputs
  • Helper text should feel attached to the thing it qualifies
  • Secondary actions should sit farther from primary content than inline metadata
  • If these relationships collapse, the UI feels improvised

When people say a layout feels "crowded," it's often not because there's too little space overall — it's because the UI uses space without telling the eye what belongs together.

Put a Control Beside What It Changes

Proximity does not only group content. It also declares which object a control acts on. People read that claim before they read the label, so a control placed next to something it does not change gets adopted by that thing.

Picture a music screen with a TV panel in the middle. A dropdown named "Friday Night Heat" sits directly under the TV, so it reads as the channel picker. It picks the playlist. Volume controls sit above the TV, so they read as TV settings. They drive the music below. Every label is correct and the screen still misleads, because position spoke first.

  • Put the control inside its target's group. The power button belongs in the TV block. The playlist picker belongs in the music block. Give each group its own container, heading, or surface to state the boundary; spacing alone works when the groups are simple. A wrapper around both groups states nothing, because it does not separate them.
  • Separate the groups by more than they separate internally. The 2× group-gap ratio in Concrete Clearances Between Targets is the mechanism that carries this rule.
  • Read the screen by position alone. Cover the labels, then name what each control changes. A control you cannot place is a control the user will misread.
  • Check the seam first. The failure lands on the last control of one group and the first control of the next. When two groups stack, the boundary control is the one the wrong neighbor claims.

Generated layouts hit this often. A model places elements in source order and never sees the rendered screen, so a control can end up beside the wrong object with its label still correct. Reading the rendered screen catches it; reading the markup does not.

Common Layout Failure Modes

  • Sloppy spacing: repeated relationships use different gaps, or unrelated relationships use the same gap. Start generously, then tighten with tokens.
  • Border bloat: every group gets another outline. Remove borders and test whether spacing, alignment, and a subtle surface still communicate the regions.
  • Content cramming: the layout tries to use every pixel. Remove low-value content, constrain line length and panel width, or reveal secondary detail progressively.
  • Pattern sprawl: one-off banners, frames inside frames, and mismatched components interrupt scanning. Prefer the fewest repeatable patterns that express the content.
  • Broken continuity: labels and values make the eye bounce across the screen or key content falls outside the obvious path. Restore shared anchors and deliberate reading order.

Reserve Space for the Scrollbar

When a scrollbar appears or disappears (navigating between a short page and a long one, opening a modal that locks body scroll), the content area widens or narrows by the scrollbar's width. Centered layouts jump sideways and the whole page reflows.

Use scrollbar-gutter: stable on the scroll container (often html or the scrolling element) so the gutter is always reserved, whether or not the scrollbar is visible. The content width stays constant and the layout no longer shifts.

html {
  scrollbar-gutter: stable;
}
  • Reach for this whenever a layout shifts as the scrollbar comes and goes.
  • Use stable both-edges if you need symmetric gutters on both sides.

With a modal library, cancel its own compensation too. Radix, and anything else built on react-remove-scroll, locks the body and adds the scrollbar's old width back as body margin. A stable gutter already reserves that space, so the two stack and the page still shifts, now in the other direction. Zero the library's margin once the gutter is doing the job:

@supports (scrollbar-gutter: stable) {
  html body[data-scroll-locked] {
    margin-right: 0 !important;
  }
}

Stack Alternate States With CSS Grid

When two states must occupy the same space, put both in one grid cell instead of absolutely positioning them. Grid keeps the container sized by its contents and leaves transform free for animation.

.stack {
  display: grid;
  place-items: center;
}

.stack > * {
  grid-area: 1 / 1;
}

Keep the natural DOM order meaningful, then add z-index only when the visual order cannot follow it. This pattern works well for crossfading icons, labels, logo sets, and other alternate states.

Only the active layer may expose semantics or focusable controls. Conditionally render inactive interactive content, or apply aria-hidden="true" and inert until that layer becomes active. Opacity and stacking order alone do not remove hidden content from the accessibility tree or tab order.

Align Section Titles With Card Content, Not the Card Edge

A section title that sits above a card belongs to the content inside the card, so it should share that content's inset. Flushing the title to the card's outer edge creates a second, competing left margin: the eye sees the title start at one x-position and every row inside the card start at another, a few pixels to the right. The card then reads as a detached box rather than the body of the thing the title names.

The rule: title padding = card content padding. Store the inset once and apply it to both, so they can never drift apart.

const INSET = 20;

function SettingsGroup({ title, children }: { title: string; children: React.ReactNode }) {
  return (
    <section>
      <h2 style={{ paddingInlineStart: INSET }}>{title}</h2>

      <div className="rounded-xl bg-card shadow-sm ring ring-border">
        <div style={{ paddingInline: INSET }}>{children}</div>
      </div>
    </section>
  );
}

Use logical properties (paddingInlineStart, paddingInline) so the alignment survives right-to-left locales.

With Tailwind there is no shared constant: ps-5 on the title and px-5 on the card body are two separate strings, so a later edit can change one and leave the other behind. Keep one source of truth by declaring the inset as a custom property on the section and reading it from both utilities:

<section className="[--inset:1.25rem]">
  <h2 className="ps-[var(--inset)]">{title}</h2>

  <div className="rounded-xl bg-card shadow-sm ring ring-border">
    <div className="px-[var(--inset)]">{children}</div>
  </div>
</section>

If you would rather stay on plain utilities, at least pick the value from the spacing scale and treat the pair as one unit, so the two classes always change together.

This shows up most in settings pages, where stacked labelled groups make any misalignment obvious down the column, but the rule holds anywhere a heading labels a card: dashboard panels, list sections, form groups, sidebar groups.

Related cases:

  • A supporting line or helper text under the title takes the same inset as the title.
  • An action button on the title row aligns to the card's opposite inset, so the row spans exactly the card's content width.
  • Full-bleed elements inside the card (dividers, row hover backgrounds, images) are the deliberate exception: they run edge to edge while text stays on the inset.

If the card's internal padding changes, the title moves with it. That is the point: one inset, one vertical line, one group.

Layout & Alignment

Alignment is a family of techniques, not one grid rule:

  • Edge alignment joins elements along a shared boundary.
  • Axis alignment organizes elements around a central spine.
  • Baseline alignment makes text and nearby controls feel seated together.
  • Mathematical alignment uses equal measured distances.
  • Optical alignment corrects for the way shape, weight, and negative space are perceived.

Good layouts use a small number of strong invisible rules. Too many local offsets make the interface feel unsettled even when every measurement can be justified.

Inspect navigation, button labels and icons, container edges, list rows, form controls, and numeric columns separately. Begin mathematically, then make optical corrections at the final size. A triangle icon may need to move farther than a square; a round icon may need to extend beyond the measured edge; uppercase text may sit differently from sentence case.

Checklist

  • Start with extra white space
  • Use spacing system consistently
  • Don't force elements to fill space
  • Clear spacing relationships between groups
  • Every control sits with the object it changes, and still reads that way with the labels covered
  • Similar roles use the same treatment; one-off patterns are justified
  • Alignment anchors and reading order form an obvious scan path
  • Common regions stay subtle and do not depend on border bloat
  • Secondary detail is removed or progressively disclosed before the layout is compressed
  • Max-width for text content (45-75 characters)
  • Internal spacing tighter than external spacing
  • Surface density appropriate to context (marketing vs admin vs settings)
  • Section titles above a card share the card content's horizontal inset, not the card's outer edge
  • Reserve scrollbar gutter (scrollbar-gutter: stable) so layout doesn't shift
  • Alternate states share a grid cell when stacking should not consume transform or remove intrinsic sizing
  • Inactive stacked layers are removed from the accessibility tree and tab order

Logical Properties and RTL

Express direction-dependent horizontal position as inline/block, not left/right, so the layout mirrors automatically under dir="rtl". The eye reads leading-to-trailing regardless of language; physical properties pin an element to a screen side and break the mirror.

Physical (avoid)Logical (use)
margin-leftmargin-inline-start
padding-rightpadding-inline-end
left: 0inset-inline-start: 0
text-align: lefttext-align: start
border-rightborder-inline-end
<!-- Good: Tailwind logical utilities, mirrors in RTL -->
<div class="ms-4 pe-6 text-start border-e">…</div>

<!-- Bad: pinned to physical sides, breaks in RTL -->
<div class="ml-4 pr-6 text-left border-r">…</div>
  • Reserve physical properties for geometry that genuinely refers to a screen side regardless of language: a device notch, a physical gesture direction.
  • Ordered UI mirrors too. When arrangement encodes progression (steppers, progress bars, star ratings), the sequence flips under RTL: stars fill from the trailing side, a stepper counts up toward the leading edge. Flexbox and grid built on logical properties mirror on their own; hand-positioned elements don't.
  • Digit order inside a number never reverses, only the elements around it. Bidi text rules live in the typography docs.

Container Queries and Content-Driven Breakpoints

Breakpoints belong to the content, not the device catalog. Break where the layout actually stops fitting, when the sidebar squeezes the content below its minimum measure or the card grid drops below a usable column width, not at 768px or 1024px because a preset says so.

Prefer container queries over viewport media queries for components. A card should adapt to the column it sits in, not to the whole window: the same card can appear full-width on one screen and inside a narrow sidebar on another, and a viewport query gets the sidebar case wrong.

/* Good: the card adapts to its own container */
.card-list { container-type: inline-size; }
@container (max-width: 400px) {
  .card { grid-template-columns: 1fr; }
}

/* Bad: viewport query collapses the card even when it has room,
   and misfires when the card is in a narrow sidebar on a wide screen */
@media (max-width: 768px) {
  .card { grid-template-columns: 1fr; }
}
  • Collapse late. Keep the expanded structure as long as it genuinely fits; premature collapsing throws away space users paid for.
  • Test the smallest supported size and the largest first, they break first, then fill in the sizes between.

Safe-Area Insets

On devices with notches, rounded corners, and gesture bars, controls flush to the viewport edge get clipped or sit under system chrome. Pad them off the physical edge with env(safe-area-inset-*) so they stay reachable.

On the web, set viewport-fit=cover on the viewport meta first, or every inset resolves to 0px and the padding below silently does nothing (see ux/mobile-web.md, Safe-Area Insets Are Zero Until You Opt In).

/* Sticky action bar clears the home-indicator gesture zone */
.action-bar {
  padding-inline: 16px;
  padding-bottom: calc(16px + env(safe-area-inset-bottom));
}

/* Floating button insets from the trailing and bottom safe areas */
.fab {
  position: fixed;
  inset-inline-end: calc(16px + env(safe-area-inset-right));
  bottom: calc(16px + env(safe-area-inset-bottom));
}
  • Content bleeds, controls float. Backgrounds, hero media, and scrollable lists extend to the viewport edge; text and controls stay inside the layout margins and safe areas, floating above the content layer.
  • Full-width buttons stay inside the layout margins, not glued to 100vw. A button can span the full content width and still keep its inline margin (start near 16px on mobile) and a visible radius. Edge-to-edge is valid only when the control is intentional platform chrome and still accounts for the safe area.
/* Good: full-width inside the margins */
.action-bar button { width: 100%; border-radius: 12px; }

/* Bad: button glued to all three edges, clips on curved corners */
.action-bar button { width: 100vw; border-radius: 0; position: fixed; bottom: 0; }

Align to Shared Edges

Pick a small set of alignment edges and put everything on them. The eye tracks straight edges to scan a column, so every stray edge, an icon 2px off the text edge, a card padded differently from its neighbor, reads as noise even when nobody can name the problem.

  • One indent step per level of subordination. Use a single project spacing step to express nesting; 16px is a useful default when no scale exists, and each deeper level repeats the same step rather than inventing a new offset.
  • Numbers right-align, text left-aligns. In tables and figure lists, numbers align to their trailing edge so digits line up by place value (see the typography docs for tabular figures); text aligns to the leading edge.
/* Good: one shared leading edge, one repeated indent step */
.section { padding-inline: 24px; }
.section .child { margin-inline-start: 16px; }

/* Bad: three unrelated leading edges in one column */
.header { padding-inline-start: 20px; }
.list-item { padding-inline-start: 14px; }
.footer { padding-inline-start: 24px; }

Concrete Clearances Between Targets

The "loosen between groups" rule needs numbers to act on. Without an established density scale, start from these clearances, then preserve a deliberately compact tool rather than expanding controls just to hit the values.

BetweenStarting point
Adjacent bordered or filled controls (buttons, inputs)12px
Around borderless controls (text buttons, icon buttons)24px
Unrelated control groups24px+ (2× the intra-group gap)

Borderless controls need more clearance because nothing marks where one target ends and the next begins: the space itself is the boundary. Pack three icon buttons at 4px and they read as one strip and collect mis-taps.

The group-gap ratio: the gap between groups is at least 2× the gap within a group. If items inside a group sit 8px apart, groups need 16px+ between them, or the eye can't tell where one group ends and the next begins.

<!-- Good: 8px within each field group, 16px between groups -->
<div class="space-y-4">
  <div class="space-y-2">…field group…</div>
  <div class="space-y-2">…field group…</div>
</div>

<!-- Good: bordered buttons at 12px, icon buttons given room -->
<div class="flex gap-3">
  <button class="rounded-lg border px-4 py-2">Cancel</button>
  <button class="rounded-lg px-4 py-2">Save</button>
</div>

<!-- Bad: uniform 12px everywhere, so nothing reads as grouped -->
<div class="space-y-3">
  <div class="space-y-3">…field group…</div>
  <div class="space-y-3">…field group…</div>
</div>

These clearances are in addition to WCAG target-size and hit-area rules (see the accessibility docs): keep the gaps so expanded hit areas never overlap.

Use this guidance in your coding agent

Install the Better Design MCP once. Your agent then loads this page with one call.

get-ui-principle({ topic: "spacing" })
claude mcp add --scope user better-design --transport http https://better-design.com/api/mcp --header "Authorization: Bearer <YOUR_API_KEY>"
Browse related design systems