---
title: "Grid & layout"
description: "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."
canonical: "https://lopifi.com/docs/foundations/grid-and-layout"
---

# 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.

Source: https://lopifi.com/docs/foundations/grid-and-layout
Part of: Lopifi — Planned: US GAAP books on autopilot, checked by a CPA (https://lopifi.com)

---

# 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.

<!-- plate:layout-lattice -->

```
--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](https://lopifi.com/md/docs/craft/responsive-and-input#touch-targets).

### 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:

<!-- plate:layout-proximity -->

| 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:
1. **Full-bleed dividers.** A `1px` rule across the full card width, between rows in a list card. Use negative margin, not reduced padding.
2. **Full-bleed media.** A chart or image that touches three edges (see media rules below).
3. **Hover and selection backgrounds** on list rows, which extend to the card edge minus the radius clearance.

Everything else respects 24.

<!-- plate:layout-card -->

---

## 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`, 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 steps down a size or the container grows. See [numerics](https://lopifi.com/md/docs/foundations/numerics#overflow).

---

## Layer 4 — type

Full detail is in [typography](https://lopifi.com/md/docs/foundations/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:
```css
.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](https://lopifi.com/md/docs/foundations/typography#the-currency-symbol)). 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](https://lopifi.com/md/docs/foundations/numerics#negative-alignment).

### 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 `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 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`, 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.
