Layout patterns
Most JS layout reads could be CSS. Plus the four states every component ships without.
## What flex and grid solve
- **Equal-width columns:** `grid-template-columns: repeat(3, 1fr);`
- **Aspect-ratio cards:** `aspect-ratio: 4 / 3;`
- **Responsive wrap:** `flex-wrap: wrap;` with `min-content`.
- **Sidebar + main:** `grid-template-columns: 240px 1fr;`
- **Centred elements:** `place-items: center;`
- **Sticky elements:** `position: sticky; top: 0;`
- **Equal heights:** `display: grid` with `align-items: stretch`.
## When JavaScript is appropriate
- **Anchor positioning** for tooltips and popovers.
- **Virtualised lists** past 50 items.
- **Drag-and-drop** hit-testing.
Batch reads and writes. Layout reads (`getBoundingClientRect`) followed by writes followed by reads cause **layout thrashing**.
## `overscroll-behavior` in modals
```css
.modal {
overscroll-behavior: contain;
}
```
Use `100dvh` instead of `100vh` on mobile. `100vh` includes browser chrome and overflows.
## Empty, loading, and error states
### Empty
- **First-time empty:** Explain the surface. Provide a CTA.
- **Filter empty:** Show what matched nothing. Offer "Clear filters."
### Loading
Show nothing for 150-300ms. Once a spinner appears, keep it 300ms minimum to avoid flicker. Prefer skeletons for lists.
### Error
State what failed, what to do, and offer an action. Preserve typed values.
## Long content safety
```css
.cell {
min-width: 0; /* allow flex/grid children to shrink */
overflow-wrap: anywhere; /* break long unbroken strings */
word-break: break-word; /* fallback for older browsers */
}
```
`min-width: 0` matters because flex/grid children default to `min-width: auto` and refuse to shrink below content width.
## Common mistakes
- **JS-measured equal heights.** Use `display: grid`.
- **`resize` event listeners for layout.** Use CSS media queries.
- **No `overscroll-behavior` on modals.** Page scrolls behind.
- **No empty state.** User sees blank whitespace.
- **Spinner for a 50ms request.** Flicker. Use a show-delay.
- **`100vh` on mobile.** Content hides behind the toolbar. Use `100dvh`.