leaf padding

One rule replaces every spacing decision: containers arrange, leaves pad. A container decides where its children go; a leaf decides how far its own content sits from its edge. Turn on the inspector to see it: amber marks a node that owns padding, red marks a negative-margin hack paying for the absence of that rule. Every section ends with the exact markup and rules that produce B, short enough to copy.

node owns padding negative-margin hack

1. parents arrange

A parent only decides where children go: flow, flex, grid. It never pads itself for a child's sake.

2. leaves inset

A leaf owns its padding: it keeps content off its own edge and knows nothing about its siblings.

3. siblings use gap

Space between siblings is the container's job, expressed as gap, never a child reaching outside itself.

4. fills are free

A fill with one child passes the job down: it stays a container and lets the leaf inset itself.

a settings card

A banner, rows, a rule and a footer button. A's owner padded itself and every full-bleed child now hacks its way out; B's card only stacks, and the footer band insets its button with a gutter spacer instead of a margin.

A: the card pads, children escape

project alpha
overview
members
billing

four hacks: the banner, the rule and the footer must climb out of the padding they never asked for. change one number, fix all four.

B: leaves inset

project alpha
overview
members
billing

same look, zero hacks. the card only stacks; the banner, rows and button pad themselves; the foot is a band whose spacer pseudo insets the button. no margin anywhere.

B markup + rules
<div class="card"> <!-- stacks; zero padding -->
<div class="card-banner">...</div> <!-- leaf: pads itself -->
<div class="card-row">...</div> <!-- leaf: pads itself -->
<div class="card-rule"></div> <!-- 1px band; no padding -->
<div class="card-foot"> <!-- band; spacer insets the button -->
<button>save</button>
</div>
</div>
.card { flex; flex-col; overflow-hidden; rounded-xl }
.card-foot { flex; min-h-12; items-center; justify-end; gap-2;
after: w-4 } <!-- gutter spacer, not a margin -->

a selectable list

Hover, active state, a count per row. A's owner padded itself, so the hover fill stops at the padding and the active bar clings to the inset.

A: the list pads

  • ana 3
  • binh 12
  • chi —

the active indicator is stranded inside the inset and hover fills look framed.

B: rows are the leaves

  • ana 3
  • binh 12
  • chi —

each row pads itself; fills run edge to edge exactly like the design.

B markup + rules
<ul class="lay-inbox"> <!-- container: stacks only -->
<li>ana <span>3</span></li> <!-- row leaf: pads itself -->
...
</ul>
.lay-inbox { flex; flex-col; overflow-hidden; rounded-xl }
.lay-inbox li { flex; justify-between; px-4; py-2 }
.lay-inbox li.is-active { border-l-2; bg-indigo-600/30 }

a media card

Cover, title, blurb, tag strip. The tag strip is a full-bleed band: A's owner padded itself, so the band has to escape.

A: the card pads

sunset over the lake

shot at golden hour from the east pier.

golden hourpier

the cover and the band both climb out; three edits per spacing change.

B: each region insets

sunset over the lake

shot at golden hour from the east pier.

golden hourpier

the body is a container that only stacks; the title and blurb pad themselves, and each tag chip is its own leaf. nothing escapes.

B markup + rules
<div class="lay-media"> <!-- stacks; zero padding -->
<div class="lay-media-cover"></div> <!-- full-bleed band -->
<div class="lay-media-body"> <!-- container: gap only -->
<h4 class="lay-media-title">...</h4> <!-- leaf: px-4 pt-4 -->
<p class="lay-media-text">...</p> <!-- leaf: px-4 pb-4 -->
</div>
<div class="lay-tags"> <!-- band: flex only -->
<span>golden hour</span> <!-- leaf: px-4 py-2 -->
</div>
</div>
.lay-media-body { flex; flex-col; gap-1 }
.lay-tags { flex }
.lay-tags span { px-4; py-2 }

a data table

Head row, zebra rows, status cells. A's owner padded itself so every cell must zero out its inset.

A: the table pads

namestatuslatency
anaok42ms
binhok38ms
chidegraded210ms

twelve cells each rewrite the same zeroing. the zebra banding stops at the inset, wrong on purpose.

B: cells are the leaves

namestatuslatency
anaok42ms
binhok38ms
chidegraded210ms

rows are containers that arrange three cells; cells inset. banding runs edge to edge.

B markup + rules
<div class="lay-table"> <!-- container: stacks only -->
<div class="lay-table-row"> <!-- row: arranges cells -->
<span class="lay-cell">name</span> <!-- leaf: pads itself -->
...
</div>
</div>
.lay-table-row { flex }
.lay-cell { flex-1; px-4; py-2 }
.lay-table-row.is-alt { bg-zinc-800/40 } <!-- banding runs full bleed -->

page color bands

Hero, body, call to action. A's owner padded itself; the colored bands (which are full-bleed by design) must hack their way back out.

A: the page pads

ship week

five services, one deadline.

deploy to prod

friday, after the freeze.

both bands fight the padding with negative margins. recolor the page and the hacks stay.

B: bands inset

ship week

five services, one deadline.

deploy to prod

friday, after the freeze.

each band is a fill with two leaves; the leaves carry the inset, the color runs full bleed.

B markup + rules
<div class="lay-band is-hero"> <!-- band: full-bleed color -->
<h4 class="lay-band-title">...</h4> <!-- leaf: pads itself -->
<p class="lay-band-text">...</p> <!-- leaf: pads itself -->
</div>
.lay-band { flex; flex-col; gap-1 }
.lay-band-title { px-6; pt-6; font-semibold }
.lay-band-text { px-6; pb-6 }

a scroll area

A sticky header over generated rows. A's owner padded itself, so the header must stick outside its own padding: a hack that breaks the moment the inset changes.

A: the scroller pads

deploys
event 1 — deployed build #121
event 2 — deployed build #122
event 3 — deployed build #123
event 4 — deployed build #124
event 5 — deployed build #125
event 6 — deployed build #126
event 7 — deployed build #127
event 8 — deployed build #128
event 9 — deployed build #129
event 10 — deployed build #130
event 11 — deployed build #131
event 12 — deployed build #132
event 13 — deployed build #133
event 14 — deployed build #134

negative top and bottom on the header so it sticks through the padding. fragile.

B: rows are the leaves

deploys
event 1 — deployed build #121
event 2 — deployed build #122
event 3 — deployed build #123
event 4 — deployed build #124
event 5 — deployed build #125
event 6 — deployed build #126
event 7 — deployed build #127
event 8 — deployed build #128
event 9 — deployed build #129
event 10 — deployed build #130
event 11 — deployed build #131
event 12 — deployed build #132
event 13 — deployed build #133
event 14 — deployed build #134

the header sticks at top-0 with its own padding; rows inset.

B markup + rules
<div class="lay-scroll"> <!-- scroller: no padding -->
<div class="lay-scroll-head">...</div> <!-- leaf + sticky top-0 -->
<div class="lay-scroll-row">...</div> <!-- leaf: pads itself -->
...
</div>
.lay-scroll { flex; flex-col; overflow-y-auto; rounded-xl }
.lay-scroll-head { sticky; top-0; px-4; py-2; bg-zinc-800 }
.lay-scroll-row { px-4; py-2; border-b }

the change cost

The same spacing change on both builds: widen the insets from 4 to 5. A's owner owns the padding, so every escaped child edits too, and each edit re-derives another node's number.

A: four coupled edits

- p-4            (the owner)
- -mx-4 -mt-4    (the banner)
- -mx-4 my-3     (the rule)
- -mx-4 -mb-4    (the footer)

miss one escape and the seam shows. the coupling is invisible until review.

B: local leaf edits

px-4 -> px-5      (each leaf, by itself)

every edit is a leaf editing itself; no rule re-derives another node's geometry, so nothing can drift out of sync.

edge rules

Four cases that look like exceptions and are not.

text next to an element

Inline flow counts as arranging: the paragraph is the leaf, the gap between words is typography, not spacing.

controls are leaves

Buttons and inputs always own their padding. A grid of buttons uses gap on the grid, never margin on the buttons.

outer spacing

The gap between a card and its page belongs to the page, not the card. Cards never carry an outside margin.

radius needs overflow

A rounded container clips its children with overflow-hidden; children may run to the edge, the radius does the rounding.

search pages

go to any page