uxskill
Star on GitHub

FIELD NOTES · 2026-05-29

Why AI-generated UIs get spacing wrong.

Look closely at a generated layout and the gaps are all the same. Sixteen pixels between the heading and the body, sixteen between the cards, sixteen inside them. It reads as competent and lands as lifeless, because spacing is the part of a design that carries rhythm, and the model has flattened it to a single value. The fix is not bigger gaps or smaller ones. It is a scale, a vertical rhythm, and a density matched to what the surface is actually for.

One value, everywhere

The tell is uniformity. A real layout uses a handful of distinct spacing steps, and the choice between them is meaningful: a section break gets more room than a paragraph break, a card's inner padding differs from the gap between cards, a label sits closer to its field than to the next group. Generated output collapses all of those decisions into one number, usually 16 or 24, applied to almost everything.

It happens for the same reason every other generated default happens. Across the training corpus, a mid-range gap is the value least likely to look broken in any single context. It is the safe centroid. Pick 16px between most things and nothing visibly collides and nothing visibly floats — so the model picks 16px between most things. The output passes a glance and fails a second look.

Spacing is rhythm, not residue

Good spacing is read the way a reader feels paragraphs and stanzas: as grouping. Elements that belong together sit close; elements that don't get air between them. That contrast is the whole point — proximity tells the eye what is one thing and what is two. When every gap is equal, the grouping signal is gone, and a screen of unrelated parts reads as one undifferentiated wall.

Uniform spacing isn't ugly. It's flat — every relationship on the screen is given the same weight, so the eye gets no help deciding what goes with what.

This is why generated layouts feel oddly tiring. The work of parsing the hierarchy, which intentional spacing should do for you, gets pushed back onto the viewer. White space is not the empty bit left over after the content is placed. It is a tool, and the absence of variation in it is a design failure, not a neutral choice.

A scale, in steps of four

The cure starts with constraint. Instead of arbitrary gaps, a real system defines a small set of steps and uses only those: 4, 8, 12, 16, 24, 32, 48, 64. Multiples of a 4px base, so every measurement lines up on a shared grid and nothing is one or two pixels off from its neighbour. A scale does two things at once — it removes the stray off-grid values that make a layout feel unaligned, and it forces each gap to be a deliberate pick from a known set rather than whatever the moment suggested.

Vertical rhythm is the second half. Section margins, heading spacing, and paragraph spacing should all be drawn from the same scale, so the page has a consistent beat down its length. A heading separated from its body by one step, sections separated by three or four — the steps differ on purpose, and because they come from one scale the differences feel composed rather than random.

Density is the axis underneath it

Here is the part the uniform-16px default cannot get right even in principle: the correct base depends on the surface. A marketing page should breathe — it has one message and a lot of room. A dashboard should be tight — its job is to fit a lot of legible information in front of someone scanning it all day. Same spacing scale, different base step, because the two surfaces sit at opposite ends of a density axis.

ux-skill treats density as a first-class, continuous dial, and it drives the spacing base directly:

surfacedensityspacing base
marketing / landinglow12px (airy)
standard appmedium6–8px
dashboard / data toolhigh4px (tight)

The base step is not a cosmetic knob. It cascades: a 4px base pulls in line-height, compresses the type scale, and tightens control targets and row heights so the whole surface reads as one coherent density. A 12px base does the reverse. Set the density once and the entire spacing system resolves to fit the surface, instead of a single gap value applied blind to a page and a cockpit alike.

What the engine emits

When you describe a project, the synthesizer compiles density into a concrete spacing scale rather than leaving it to taste at generation time. You ask for surface=dashboard, tone=["serious", "dense"] and it resolves:

  1. Density resolves high, so the spacing base is 4px and the scale steps tighten to match.
  2. Section, heading, and paragraph spacing are emitted as named steps off that base, so the vertical rhythm is built in, not improvised per component.
  3. The linter then guards it: every off-scale value — a stray 13px gap, a 23px margin — is flagged as a stray, because off-grid spacing is the fingerprint of the no-system default.

The same request, run again next week on a different machine, returns the same scale. Deterministic, offline, no model in the loop — so the spacing rhythm is a property of the system, not a roll of the dice.

The point

Generated layouts feel flat because the model applies one safe gap to every relationship on the screen, and a single value cannot encode rhythm or match a surface's density. Give the brief a spacing scale tied to a density axis, draw the vertical rhythm from that one scale, and treat white space as something you spend deliberately rather than something left over. The layout stops reading as a competent draft and starts reading as a designed thing.

pip install uxskill
# then, in your AI coding tool:
# /ux-recommend  — density-resolved spacing scale, deterministic

Related