Grid & layout
Every Lopifi component is designed in four layers, in order: base grid, safe space, structure, type. Type comes last. That order keeps the system coherent when someone new adds a component.
The four-layer method#
Layer 1 ▸ BASE GRID 8px lattice. Nothing else exists yet.
Layer 2 ▸ SAFE SPACE Padding and grouping. Where content may live, and what belongs with what.
Layer 3 ▸ STRUCTURE Text slots, media placeholders, controls. Gray boxes only.
Layer 4 ▸ TYPE Actual sizes and weights. Max 3 of each.
Each layer gets its own artboard in the design file and its own commit in the build, so a review can reject one layer without touching the ones after it.
Layer 1 — the base grid#
8px base unit. A coarser grid (12, 16) makes small controls impossible. A finer one (2, 4) stops constraining anything. 8 is the tightest unit that still constrains.
The spacing scale
--space-0: 0px
--space-0-5: 4px /* half-step — restricted, see below */
--space-1: 8px
--space-2: 16px
--space-3: 24px
--space-4: 32px
--space-5: 40px
--space-6: 48px
--space-8: 64px
--space-10: 80px
--space-12: 96px
--space-16: 128px
The half-step rule#
4px is restricted to the inside of controls ≤ 32px tall: badge padding, pill insets, the icon-to-label gap in a small button, checkbox-to-label. It never appears in layout. No 4px gaps between cards, no 12px card padding, no 20px section spacing.
Why: small controls can't always be padded on an 8px lattice. A 32px control works (8px vertical padding + 16px line-height), but a 24px control needs 4px. Anything larger than a control stays on 8.
Forbidden values: 6, 10, 12, 14, 18, 20, 28, 36, 44. If a design wants 12px, use 8 or 16 and figure out which grouping was wrong.
Heights are on the grid too#
Control heights 24 (xs) · 32 (sm) · 40 (md, default) · 48 (lg)
Table row heights 32 (compact) · 40 (default) · 48 (comfortable)
Header / toolbar 56
App top bar 64
Mobile tab bar 56 + safe-area-inset-bottom
md (40px) is the default because it's the smallest height that clears a 44px touch target once you count its 4px of hit-slop. See responsive & input.
Layout grid#
- Desktop / laptop: 12 columns, 24px gutter, 24px page margin, max content width 1440. Below 1440 the grid is fluid. Above it, content centers and the canvas continues.
- Tablet: 8 columns, 24px gutter, 24px margin.
- Phone: 4 columns, 16px gutter, 16px margin. This is the one place the page margin isn't 24: on a 390px screen, 24px margins cost 12% of the viewport.
Layer 2 — safe space and grouping#
Safe space is padding, and it's also how you show what belongs together. Two elements 8px apart read as one thing. At 24px they read as two. That's the whole hierarchy mechanism at this layer.
The inset scale#
Card / panel inset 24px ← the system default
Compact card inset 16px (dense mode, or cards < 240px wide)
Dialog inset 24px
Sheet inset 24px (16px on phone)
Table cell inset 12px v / 16px h — the one sanctioned 12, see note
Toolbar inset 16px
Popover / menu inset 8px (items get 8v/12h)
Table cell vertical padding is 12px. This is the only layout exception to the half-step rule, and it's deliberate: a 40px row with 16px line-height needs 12px above and below. Everything else obeys.
The proximity ladder#
Distance encodes relationship. Use only these five:
Distance is the relationship
| Gap | Meaning | Example |
|---|---|---|
| 4px | Same object | Currency symbol → figure; icon → its label |
| 8px | Same group | Label → value; two stacked metadata lines |
| 16px | Related, separate | Two fields in a form; card header → card body |
| 24px | Distinct groups | Two cards in a grid; form section → form section |
| 32–48px | Page sections | Statement section → next section; hero → content |
If you need a sixth, one of your groupings is wrong.
Safe space is not negotiable at the edge#
The 24px card inset is a hard boundary. Only three things may bleed into it:
- Full-bleed dividers. A
1pxrule across the full card width, between rows in a list card. Use negative margin, not reduced padding. - Full-bleed media. A chart or image that touches three edges (see media rules below).
- Hover and selection backgrounds on list rows, which extend to the card edge minus the radius clearance.
Everything else respects 24.
Safe space, at the edge
Layer 3 — structure#
Gray boxes only. No type, color or icons. You're deciding how many things there are and where they sit. This is where over-design gets caught: a fourth text slot is hard to justify when it's an unlabeled gray rectangle.
Every structure spec names its slots:
BalanceCard
├── eyebrow ▸ optional · 1 line · truncate
├── figure ▸ required · 1 line · never truncates, shrinks instead
├── delta ▸ optional · 1 line
├── media ▸ optional · sparkline, full-bleed bottom
└── actions ▸ optional · max 2, one primary
Structural rules#
- Max 3 text slots per component. A fourth is a sub-component.
- Every slot declares its overflow behavior:
truncate,line-clamp-2,shrink, ornever overflows(with the constraint that guarantees it). - Media placeholders declare aspect ratio, never fixed height.
aspect-[16/9],aspect-[1.586]for cards,aspect-[3/1]for sparkline strips. - Controls declare their count ceiling. "Max 2 actions, at most 1 primary." Enforce it in the type signature where you can.
- The figure never truncates. In a finance component, a truncated number is a bug. It steps down a size or the container grows. See numerics.
Layer 4 — type#
Full detail is in typography. The rule that lives here:
Max 3 font sizes and 3 font weights per component.
Per component, not per page. With only three sizes, you have to spend them on the three things that matter. That's what creates hierarchy.
A component that needs four sizes is two components.
Optical alignment#
The grid gets you 95% of the way. These fixes cover the gap between "aligned" and "looks aligned."
Icons align to cap height, not line box#
If you align boxes, a 16px icon next to 14px/20px text sits 2px above the text's optical center. Align the icon's center to the center of the text's cap height instead:
.icon-inline { transform: translateY(-0.5px); } /* 16px icon, 14px/20 Inter */
Measure each size pair and document the offset. Lopifi's values:
| Icon | Text | Offset |
|---|---|---|
| 16px | 13px/16 | -0.5px |
| 16px | 14px/20 | -0.5px |
| 20px | 16px/24 | -1px |
| 24px | 20px/28 | -1px |
Right-aligned numbers hug the inset#
The last column of a numeric table aligns its right edge to the card inset, not to cell padding inside it. The most common table mistake is a number that stops 16px short of the card edge while its header touches it.
Currency symbols are optically offset#
$ is taller than lining figures and sits differently from € and £. The symbol is always set lighter in weight and color (see typography). Give it a 4px gap and baseline-align it. Don't try to center it vertically.
Trailing punctuation and parentheses hang#
In a right-aligned column with both 1,234.00 and (1,234.00), the closing paren pushes the digits left by ~4px and breaks decimal alignment. Always reserve the paren column: render a transparent ) for positive values, or use a fixed-width paren cell. Details in numerics.
Optical centering in circles#
An icon in a circular avatar, or a play or chevron glyph, needs +0.5px to +1px toward the direction it points. Chevrons in > accordions get translateX(0.5px).
Density modes#
Three modes, set with a data-density attribute on any container. Descendants inherit it.
compact |
default |
comfortable |
|
|---|---|---|---|
| Row height | 32 | 40 | 48 |
| Card inset | 16 | 24 | 24 |
| Body size | 13/16 | 14/20 | 14/20 |
| Cell h-padding | 12 | 16 | 16 |
| Gap ladder | ×0.75 (rounded to 4) | ×1 | ×1.25 |
compact is for power users reviewing hundreds of rows on large displays. comfortable is the phone default. Density controls how much information fits, not how things look. Persist it per user, and every component that renders a list must respect it.
Layout composition rules#
- Cards in a grid are equal-height by default. Use
gridwithauto-rows-fr, not flex-with-stretch, so the figure baselines align across the row. - Never nest more than two card levels. A third level should be a section with a rule line.
- Sticky elements are on the grid too. A sticky table header is exactly one row height. A sticky total footer is exactly one row height + 1px top rule.
h-dvh, neverh-screen. Mobile browser chrome makes100vhwrong.- Fixed elements respect
env(safe-area-inset-*): bottom bars, floating action buttons, sticky totals. - Scroll containers own their own padding. Padding on a scroll parent clips the shadow of scrolled content. Put it on the inner track.