layout: form layouts, padding lives only on leaves

Forms are where the leaf-only rule earns its keep: labels, inputs, step bars and edit affordances all want to run edge to edge. Side A pads a container and pays in negative margins; side B hands the padding to the leaves. Turn on the inspector: amber owns padding, red pays for it.

label placement under a width budget

Drag the slider to resize both forms. Names on top survive any width; names beside the input are a grid decision that reflows with a container query, not a per-field edit.

form width280 – 540

A: names above inputs

gutter tracks inset every field; nothing pads but the controls. it reads the same at 280 and at 540.

B: names beside, reflowing

full name
work email
team

below 420px the grid collapses to names-on-top by itself: one rule, zero per-field edits.

ruling

whether a field's name sits above or beside its input is the form grid's arrangement, never the field's padding. switch placement in one grid rule; if you find yourself editing every field, the padding is on the wrong node.

a multi-step wizard

Step bar, body, footer buttons. A pads the whole wizard; the bar and the footer must hack their way out to run full bleed.

A: the wizard pads

1name the workspace — you can rename it later.2pick a plan.3confirm and create.

name the workspace — you can rename it later.

the bar escapes up and out; the footer escapes down and out.

B: three bands

1name the workspace — you can rename it later.2pick a plan.3confirm and create.

name the workspace — you can rename it later.

bar, body and footer are bands; the fields and buttons are leaves inset by gutter tracks and spacers. zero margins.

ruling

a wizard is three full-bleed bands. the bar and footer own their edges, and the body insets its field with gutter tracks. when the container pads, every band must escape, and every escape is a negative margin that drifts the day the inset changes.

B markup
<div class="lay-wizard"> <!-- container: bands only, zero padding -->
<div class="lay-steps">…</div> <!-- band; its leaves inset themselves -->
<div class="lay-wizard-body"> <!-- gutter tracks -->
<p data-wizard-text>…</p>
<Input className="col-start-2 w-full" defaultValue="acme-platform" />
</div>
<div class="lay-wizard-foot"> <!-- band; spacer pseudos inset the buttons -->
<Button variant="ghost" size="sm">back</Button>
<Button size="sm">next</Button>
</div>
</div>
.lay-wizard-body { grid; grid-cols-[1.5rem_minmax(0,1fr)_1.5rem]; gap-y-5 }
.lay-wizard-body > * { col-start-2 }
.lay-wizard:not(.is-inset) > .lay-wizard-foot { grid; min-h-16; justify-between;
before/after: w-6 }

a settings page

A nav column and a settings panel with grouped rows. A pads both containers; the active fill and every divider escape.

A: the containers pad

notificationson
weekly digeston
beta featuresoff

the nav links and both dividers fight the padding; hover fills stop at the inset.

B: rows and links inset

notificationson
weekly digeston
beta featuresoff

divide-y owns the separators; links and rows pad themselves; zero hacks.

ruling

the row and the link are the leaves; the panel and nav only arrange and divide. padding on the container pushes the active and hover fills inward: a fill that stops short of the edge is how you can see the padding sits on the wrong node.

B markup
<div class="lay-panel"> <!-- container: divide-y only -->
<div class="lay-setting"> <!-- row leaf: pads itself -->
<span class="lay-setting-name">notifications</span>
<Badge variant="default" className="shrink-0">on</Badge>
</div>
…
.lay-panel { divide-y }
.lay-setting { flex; justify-between; px-4 }
.lay-setting-name { py-3 } <!-- x inset comes from the row, once -->

inline edit

Toggle edit mode: a view span swaps for an input in the same footprint. A's cell owns the padding, so the input must escape it to match the span.

A: the cell pads, the input escapes

anaadminactive
binheditoractive

the input climbs out with negative margins to cover the same pixels, and drifts the moment the cell inset changes.

B: view and edit share the footprint

anaadminactive
binheditoractive

span and input both pad identically inside a bare cell; the swap is pixel-stable at any inset.

ruling

a view/edit swap is pixel-stable only when a bare cell hosts two leaves that pad identically. pad the cell and the input must escape it with negative margins: correct today, drifted at the next inset change.

B markup
<td class="is-slot"> <!-- bare cell: no padding -->
<span class="lay-view" data-edit="b1">admin</span> <!-- leaf: px-4 py-3 -->
<input class="lay-edit is-hidden" data-edit="b1"> <!-- leaf: px-4 py-3 -->
</td>
.lay-swap td.is-slot { px-0 py-0 }
.lay-swap .lay-view { block; px-4 py-3 }
.lay-swap .lay-edit { w-full; bg-zinc-800; px-4 py-3 }

containers arrange, leaves inset. side B: zero negative margins.

search pages

go to any page