Minimal DOM
Fewest DOM nodes with React and Tailwind
Same UI, fewest DOM nodes — every element earns its place. If deleting it breaks nothing (semantics, layout, behavior, required styling), it must not exist.
MUST
- Keep a node ONLY if it provides one of: semantics/a11y (
ul/li,button,label,form,nav,section, ARIA, focus); a layout constraint (own containing block / positioning / clip / scroll / stacking —relative,overflow-*,sticky,z-*,min-w-0); behavior (measurement ref, observer, portal, event boundary, virtualization); or component API (can’t pass props/classes to the real root after tryingas/asChild/forwarding). Why: every node is render + memory cost. - Spacing via parent
gap-*(flex/grid) orspace-x/y-*. Why: no wrapper for gaps. - Separators via parent
divide-y/divide-x. Why: no separator elements. - Alignment via
flex/gridon the existing parent. Why: no alignment wrapper. - Visual (padding/bg/border/shadow/radius) on the element that owns the box. Why: no decoration wrapper.
- Group JSX with
<>...</>fragment, not<div>. Why: zero DOM cost. - Style mapped components by passing
classNameto the item; uniform direct children via*:or[&>tag]:. Why: props first, selectors second — no repeat-class wrapper.
NEVER
- Add a wrapper a
gap/space/divide/className/[&>...]:could replace. Cost: dead node, render + read budget.
Examples
| Good (selector pushdown) | Bad (repeated classes) |
|---|---|
<div className='divide-y [&>p]:px-3 [&>p]:py-2'> | <div className='divide-y'> with px-3 py-2 on each <p> |
Pitfall
- Selector tools:
*:direct children ·[&>li]:py-2targeted ·[&_a]:underlinedescendant (sparingly) ·group/peeron existing nodes →group-hover:*/peer-focus:*·data-[state=open]:*/aria-expanded:*/disabled:*·first:/last:/odd:/even:/only:structural. - Review each node: can I delete it → delete; can
gap/space/dividereplace it → do it; can I passclassName→ do it; can[&>...]:remove repetition → do it.