structural mechanics
eight mechanics: sticky, nested scroll, fixed bars, safe areas, reflow, container queries, stacking, aspect ratios. the wrong side is the padded-container way, the right side is leaf-only. tick the inspector to see who owns padding.
sticky stacks
two sticky bars in one scroller, then sticky groups with a sticky first column.
wrong · the scroller pads
| #1000 | customer 0 | north | $10 | open |
| #1001 | customer 1 | north | $17 | open |
| #1002 | customer 2 | north | $24 | open |
| #1003 | customer 3 | north | $31 | open |
| #1004 | customer 4 | north | $38 | open |
| #1005 | customer 5 | north | $45 | open |
| #1006 | customer 6 | north | $52 | open |
| #1007 | customer 7 | north | $59 | open |
| #1008 | customer 8 | north | $66 | open |
| #1009 | customer 9 | north | $73 | open |
| #1010 | customer 10 | north | $80 | open |
| #1011 | customer 11 | north | $87 | open |
| #1012 | customer 12 | north | $94 | open |
| #1013 | customer 13 | north | $101 | open |
| #1014 | customer 14 | north | $108 | open |
| #1015 | customer 15 | north | $115 | open |
| #1016 | customer 16 | north | $122 | open |
| #1017 | customer 17 | north | $129 | open |
| #1018 | customer 18 | north | $136 | open |
| #1019 | customer 19 | north | $143 | open |
| #1020 | customer 20 | north | $150 | open |
| #1021 | customer 21 | north | $157 | open |
| #1022 | customer 22 | north | $164 | open |
| #1023 | customer 23 | north | $171 | open |
the sticky bars must out-pad the scroller with negative margins, and their stacking order is fragile.
right · the bars pad
| #1000 | customer 0 | $10 | open |
| #1001 | customer 1 | $17 | open |
| #1002 | customer 2 | $24 | open |
| #1003 | customer 3 | $31 | open |
| #1004 | customer 4 | $38 | open |
| #1005 | customer 5 | $45 | open |
| #1006 | customer 6 | $52 | open |
| #1007 | customer 7 | $59 | open |
| #1008 | customer 8 | $66 | open |
| #1009 | customer 9 | $73 | open |
| #1010 | customer 10 | $80 | open |
| #1011 | customer 11 | $87 | open |
| #1012 | customer 12 | $94 | open |
| #1013 | customer 13 | $101 | open |
| #1014 | customer 14 | $108 | open |
| #1015 | customer 15 | $115 | open |
| #1016 | customer 16 | $122 | open |
| #1017 | customer 17 | $129 | open |
| #1018 | customer 18 | $136 | open |
| #1019 | customer 19 | $143 | open |
| #1020 | customer 20 | $150 | open |
| #1021 | customer 21 | $157 | open |
| #1022 | customer 22 | $164 | open |
| #1023 | customer 23 | $171 | open |
the scroller pads nothing; the group is a container (flex col), the bars are leaves that pad and stick as a unit.
<div class="lay-stack-b"> <!-- container: no padding --><div class="lay-stackgroup"> <!-- one sticky unit --><div class="lay-stackhead">north</div> <!-- leaf: owns the inset --><div class="lay-stacksub">revenue</div></div><table class="lay-table">...</table></div>
/* globals.css, mechanics 1 */.lay-stack-b { flex flex-col h-72 overflow-auto rounded-xl bg-paper-quiet; } /* no p-4 */.lay-stackgroup { sticky top-0 z-20 flex flex-col; }.lay-stackhead { bg-indigo-700 px-4 py-2 font-semibold; } /* the leaf pads */.lay-stacksub { bg-zinc-800 px-4 py-1 text-ink-muted; }
nested scroll
a modal whose list scrolls while the modal stays put. the list is a flex child with min-h-0.
wrong · the modal pads
- invite 1
- invite 2
- invite 3
- invite 4
- invite 5
- invite 6
- invite 7
- invite 8
the list needs negative margins to reach the modal's edges, and -mx breaks the scrollbar's right inset.
right · the list pads
- dana · viewer
- leo · editor
- mia · viewer
- raj · editor
- zoe · viewer
- kim · admin
- ana · viewer
- tom · editor
min-h-0 + overflow-auto on the list; title and foot are leaves that pad. overscroll-contain keeps the wheel inside the list.
<div class="lay-modal"> <!-- container: no padding, no gap --><b class="lay-modtitle is-inset">invites</b><ul class="lay-modlist"><li class="lay-modrow">dana · viewer</li></ul><div class="lay-modfoot-in"><Button variant="ghost" size="sm">cancel</Button><Button size="sm">invite</Button></div></div>
/* globals.css, mechanics 2 */.lay-modal { flex max-h-64 w-64 flex-col overflow-hidden rounded-xl; } /* no p-6 */.lay-modlist { flex min-h-0 flex-1 flex-col overflow-auto overscroll-contain; }.lay-modrow { px-6 py-1; } /* each row is a leaf */.lay-modfoot-in { flex min-h-12 items-center justify-end gap-2border-t border-rule after:w-3 after:content-['']; } /* band: gap + spacer, no button margins */
fixed bottom bar
a bar pinned to the bottom of a scroller: sticky bottom works only without container padding.
wrong · the page pads
the bar needs -mb and a padding fudge to clear; scroll to the end and watch the gap jump.
right · the shell splits
the scroller is a flex child (min-h-0), the bar sits after it: nothing sticks, nothing overlaps.
<div class="lay-page-b"> <!-- container: no padding --><div class="lay-pagescroll"> <!-- min-h-0 flex child --><div class="lay-pageitem-in">cart item 1</div></div><div class="lay-pagebar"> <!-- sits after the scroller --><Button size="sm">checkout</Button></div></div>
/* globals.css, mechanics 3 */.lay-page-b { flex h-72 flex-col overflow-hidden rounded-xl bg-paper-quiet; } /* no p-4 pb-20 */.lay-pagescroll { flex min-h-0 flex-1 flex-col overflow-auto; }.lay-pageitem-in { shrink-0 px-4 py-2; }.lay-pagebar { flex min-h-14 items-center justify-end border-t bg-paper/90after:w-4 after:content-['']; } /* no sticky bottom, no -mb */
safe area
a phone frame with fake insets (--sat / --sab). content clears the notch and the home indicator with max(env, inset) padding on leaves.
wrong · magic offsets
balance $1,204.00
card 4021
limit $5,000
statement july
home
cards
stats
-mt-11 pt-11 is a hardcoded guess; change the notch and every offset is wrong.
right · inset leaves
balance $1,204.00
card 4021
limit $5,000
statement july
home
cards
stats
the safe top and bottom are leaves of exactly max(env(), inset) height. on a real device env() takes over and nothing else changes.
<div class="lay-phone"> <!-- container: no pt/pb --><div class="lay-phonehead"><span class="lay-safetop"></span> <!-- spacer leaf --><b class="lay-phoneheadtitle">wallet</b></div><div class="lay-phonetabs"><div class="lay-phonetabsrow">...</div><span class="lay-safebottom"></span> <!-- spacer leaf --></div></div>
/* globals.css, mechanics 4 */.lay-phone { relative flex h-[420px] w-56 flex-col overflow-hidden; } /* no pt/pb guess */.lay-safetop { h-[max(env(safe-area-inset-top),var(--sat))]; }.lay-safebottom{ h-[max(env(safe-area-inset-bottom),var(--sab))]; }
responsive reflow
one component, three states: desktop table, drawer nav, card list: all from the container's width, not the viewport. drag the slider or resize-x.
wrong · viewport queries
| invoice | customer | total | status |
|---|---|---|---|
| #1042 | acme | $120 | paid |
| #1041 | globex | $80 | due |
| #1040 | initech | $310 | paid |
| #1039 | umbrella | $45 | overdue |
| #1038 | tyrell | $220 | paid |
the sidebar uses md:flex (a viewport test). shrink the browser and the layout reflows even though the container is still wide.
right · container queries
| invoice | customer | total | status |
|---|---|---|---|
| #1042 | acme | $120 | paid |
| #1041 | globex | $80 | due |
| #1040 | initech | $310 | paid |
| #1039 | umbrella | $45 | overdue |
| #1038 | tyrell | $220 | paid |
@container variants: the table shows at @md, the card grid below it, the drawer docks statically at @2xl. the menu button toggles the drawer at narrow widths.
<div class="lay-reflow"> <!-- @container, no padding --><aside class="lay-refdrawer" data-drawer><span class="lay-refitem">dashboard</span></aside><div class="lay-refmain"><div class="lay-refmenurow"><Button variant="ghost" size="sm">menu</Button></div><table class="lay-reftable is-desktop">...</table><div class="lay-refcards">...</div></div></div>
/* globals.css, mechanics 5 */.lay-reflow { @container relative flex w-[820px] resize-xoverflow-hidden rounded-xl; } /* no p-4 */.lay-reftable.is-desktop { hidden @md:table; } /* container query, not md: */.lay-refdrawer { absolute inset-y-0 @2xl:static @2xl:flex; }.lay-refcards { grid gap-px @md:hidden; }
container-query card
the same card goes row at @sm and stacks below it. resize-x the card.
wrong · viewport queries
the media block's negative margins are glued to the card's padding; stack vs row flips on the viewport, not the card.
right · container queries
@sm: the media squares off and docks left; below it, it bleeds to the card's top edge on its own.
<div class="lay-cqcard"> <!-- @container, no padding --><div class="lay-cqrow"><span class="lay-cqmedia"></span><span class="lay-cqtext is-center"><b class="lay-cqname">compact card</b><span class="lay-cqmeta">resizes with its box</span></span></div></div>
/* globals.css, mechanics 6 */.lay-cqcard { @container min-w-48 w-[320px] max-w-full resize-xoverflow-hidden rounded-xl; } /* no p-4 */.lay-cqrow { flex flex-col @sm:flex-row; }.lay-cqmedia { aspect-video bg-gradient @sm:aspect-square @sm:w-40; } /* no -mx -mt */.lay-cqname { px-4 pt-3; }.lay-cqmeta { px-4 pb-3 text-ink-muted; }
z-index layers
left: magic numbers escaping the page. right: one local scale inside isolate. uncheck layers to hide them.
wrong · magic numbers
covers everything, escapes every ancestor
z-999 beats z-50 by accident; the next feature needs z-9999. the scale is unbounded and global.
right · one local scale
isolate caps the scale: 10..40 inside the stage, nothing leaks out, nothing leaks in.
<div class="lay-zstage is-scale"> <!-- isolate: one local scale --><div class="lay-zhead" data-z="head"><span class="lay-zlabel">app bar · z-10</span></div><div class="lay-zdrop" data-z="dropdown">...</div><div class="lay-zmodal" data-z="modal">...</div><div class="lay-ztoast" data-z="toast">...</div></div>
/* globals.css, mechanics 7 */.lay-zstage.is-scale { isolate flex flex-col; } /* caps the scale */.lay-zhead { sticky top-0 z-10; }.lay-zdrop { absolute left-4 top-12 z-20; }.lay-zmodal { absolute inset-8 z-30 shadow-2xl; }.lay-ztoast { absolute bottom-4 right-4 z-40; }
aspect ratio
a 16:9 media block: the padding hack versus one property.
wrong · padding-top hack
56.25% padding-top fakes the ratio; the image needs absolute positioning to climb back in.
right · aspect-ratio
aspect-video reserves the box; the image is a normal child.
<div class="lay-imgcard"> <!-- container: no padding --><img class="lay-img" src="..." alt=""><p class="lay-imgcap">aspect-video reserves the box...</p></div>
/* globals.css, mechanics 8 */.lay-imgcard { flex flex-col overflow-hidden rounded-xl bg-paper-quiet; } /* no p-4 */.lay-img { aspect-video w-full object-cover; } /* not pt-[56.25%] + absolute */.lay-imgcap { px-4 py-3; }