Skip to content

Grid & layout

Every component in Lopifi is designed in four layers, in order. You do not skip ahead. Layer 4 (the actual type) is the last decision, not the first — which is why the system stays coherent when a new component is added by someone who wasn't in the original room.


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. Grey boxes only.
Layer 4  ▸  TYPE             Actual sizes and weights. Max 3 of each.

Each layer is a separate artboard in the design file and a separate commit in the build. A component review can reject at any layer without touching the ones after it.


Layer 1 — the base grid

8px base unit. Set as tightly as possible to preserve flexibility later — a coarse grid (12, 16) makes small controls impossible; a fine grid (2, 4) stops being a grid at all. 8 is the tightest unit that still constrains.

--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 exists but is restricted to the interior of controls ≤ 32px tall — badge padding, pill insets, icon-to-label gaps inside a small button, checkbox-to-label. It may never appear in layout: no 4px gaps between cards, no 12px card padding, no 20px section spacing.

Rationale: a 32px control cannot be padded on an 8px lattice without becoming visually wrong (8px vertical padding + 16px line-height = 32px works; but a 24px control needs 4px). Everything above control scale stays on 8.

Forbidden values: 6, 10, 12, 14, 18, 20, 28, 36, 44. If a design wants 12px, the answer is 8 or 16, and the question is 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 control height because it is the smallest height that comfortably clears a 44px touch target once its 4px of ambient hit-slop is counted — 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, centered with the canvas continuing.
  • Tablet: 8 columns, 24px gutter, 24px margin.
  • Phone: 4 columns, 16px gutter, 16px margin. Note the margin drops to 16 — this is the one place the page margin leaves 24, because 24px margins on a 390px screen cost 12% of the viewport.

Layer 2 — safe space and grouping

Safe space is the padding and the proof that things belong together. Two elements 8px apart are one thing; 24px apart they are two things. That is the entire 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 sole layout exception to the half-step rule and it is deliberate: a 40px row with 16px line-height needs 12px above and below. Documented as an exception rather than silently violated. Everything else obeys.

The proximity ladder

Distance encodes relationship. Use only these five:

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. Nothing bleeds into it except three sanctioned cases:

  1. Full-bleed dividers — a 1px rule that spans the full card width, used between rows in a list card. Achieved with negative margin, not reduced padding.
  2. Full-bleed media — a chart or image that touches three edges (see media rules below).
  3. Hover/selection backgrounds on list rows, which extend to the card edge minus the radius clearance.

Everything else respects 24.


Layer 3 — structure

Grey boxes. No type, no color, no icons. You are deciding how many things there are and where they sit — and this is where most components are silently over-designed, because it's very hard to justify a fourth text slot when it's an unlabeled grey rectangle.

Every Lopifi component's structure spec names its slots explicitly:

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, or never 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 shrinks (via a size step-down) or the container grows. See numerics.

Layer 4 — type

Covered fully in typography. The rule that lives here:

Max 3 font sizes and 3 font weights per component.

Not per page — per component. This is the constraint that generates hierarchy, because with only three sizes available you are forced to spend them on the three things that actually matter.

A component that genuinely needs four sizes is two components that should be composed.


Optical alignment

The grid gets you 95% there. These fix the rest, and they are the difference between "aligned" and "looks aligned".

Icons align to cap height, not line box

A 16px icon next to 14px/20px text sits 2px above the text's optical center if you align boxes. Align the icon's optical center to the text's cap height center:

.icon-inline { transform: translateY(-0.5px); } /* 16px icon, 14px/20 Inter */

Measure per size pair; 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 any numeric table aligns its right edge to the card inset, not to a cell padding inside it. A number that stops 16px short of the card edge while the header above it touches the edge is the most common table-craft failure.

Currency symbols are optically offset

$ is taller than lining figures and sits differently from € and £. When the symbol is set at a lighter weight/color (which it always is — see typography), give it a 4px gap and let it baseline-align; do not attempt vertical centering.

Trailing punctuation and parentheses hang

In a right-aligned column containing both 1,234.00 and (1,234.00), the closing paren pushes the digits left by ~4px and destroys decimal alignment. Fix: reserve the paren column always — render a transparent ) for positive values, or use a fixed-width paren cell. Detail in numerics.

Optical centering in circles

An icon inside a circular avatar or a play/chevron glyph needs +0.5px to +1px toward the direction it points. Chevrons in > accordions get translateX(0.5px).


Density modes

Three modes, set by a data-density attribute on any container, cascading to descendants.

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 on large displays reviewing hundreds of rows. comfortable is the phone default. Density is not a visual preference — it's an information-throughput setting, and it must be persisted per user and respected by every component that renders a list.


Layout composition rules

  • Cards in a grid are equal-height by default. Use grid with auto-rows-fr, not flex-with-stretch, so the figure baselines align across the row.
  • Never nest more than two card levels. A card inside a card inside a card is a hierarchy failure; the inner one 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, never h-screen. Mobile browser chrome makes 100vh wrong.
  • 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.

Told when this spec changes

The docs are the contract the components are built against. When one changes, the component changes with it — and the copy in your repo does not. One email when that happens.

One address, no list rental, unsubscribe in one click.

Works in your QuickBooks Online or Xero file

Get your books closed every month

Lopifi is building AI bookkeeping that keeps your books closed on US GAAP as transactions happen, with a CPA review every quarter, for $200 a month. We’re not open yet.

$200/month when we open. No card now.