Icons, spacing, radius, shadow
Icons — one library, fixed sizes
lucide-react, and nothing else. Line icons, stroke 2.0 (2.4 for emphasis).
Never mix filled and line icons in one component. Never emoji, never a second
icon pack, never a stock icon set.
The sizes are already set by the primitives. Match them when you place an icon by hand:
| Context | Size | Class |
|---|---|---|
Inside a button, input, select, menu row, rail row | 16px | size-4 (the primitives apply it automatically) |
A meta glyph beside a label (stat tile, table cell) | 14px | size-3.5 |
Inside a badge | 12px | size-3 |
Empty-state medallion | 20px in a 40px circle | size-5 |
Button size xs | 12px | applied by the variant |
Gap between an icon and its label is 8px (gap-2); dense rows use 6px
(gap-1.5). The gap is on the flex container, never a margin on the icon.
Hit targets are 44px, always. Controls smaller than that paint an invisible
44px pointer target (presets.squareTouchTarget) — the visual box shrinks, the
hit area does not. This is already inside Button icon-* sizes, Switch, and the
compact Selects. If you build a new small control, use the same preset.
Icons carry meaning, not decoration. A row of icons that all mean "this is a thing" is data slop — delete them.
Spacing — multiples of 4
4, 8, 12, 16, 20, 24, 32, 40, 48, 64, 80, 96, 128 (--space-1 … --space-32).
Nothing between them.
Defaults that recur:
| What | Value |
|---|---|
Page padding | 24px, 32px from lg (Screen) |
Gap between blocks on a scrolling page | 40px ( gap-10) |
Gap between blocks on a viewport page | 24px ( gap-6) |
Card padding | 20–24px |
Table cell padding | 20px horizontal ( px-5) |
Icon → label | 8px |
Marketing section padding | 96 / 64 / 48px (desktop / tablet / mobile) |
Max content width | 1280px centred |
Lay out sibling groups with flex/grid and gap, never with margins on
children or whitespace between inline elements. Gap survives reorder, delete and
duplicate; the other two do not.
Radius — measured, not assumed
These are the resolved values, read off the rendered page
(14-measurements.md), not the ones a component comment claims:
| Element | Class | Measured |
|---|---|---|
Button, input, select, menu item, rail row | rounded-md | 6 px |
Card, panel, alert, empty state, popover | rounded-lg | 8 px |
Card at the brand scale (a tile you click) | rounded-[var(--radius-card)] | 16 px |
Icon chip, account button | rounded-[var(--radius-input)] | 10 px |
Mono badge, score tile | rounded-[var(--radius-badge)] | 8 px |
Pill, chip, avatar, switch | rounded-full | full |
A track or bar that must read as a capsule | rounded-[var(--radius-pill)] | full (9999px) |
A tooltip arrow | rounded-[2px] | 2 px |
A checkbox box | rounded-[4px] | 4 px |
A legend swatch beside a label | rounded-[3px] | 3 px |
Tailwind's own --radius-md / --radius-lg defaults (6 and 8 px) are what
rounded-md / rounded-lg resolve to here — the @theme block does not
override them, and --radius: var(--radius-card) (16 px) only feeds the
explicit var() uses. A comment in button.tsx says "8px controls, 10px
containers"; it is inherited from the app this file came from and the rendered
page disagrees with it. Trust the measurement.
A 16px checkbox at the 6px control radius reads as a rounded button; at 4px it reads as a box you tick. It is the one control that is smaller than its own radius step.
The two tiny values are deliberate exceptions, not a third step: a 2 px arrow and a 3 px swatch are marks, not surfaces, and rounding them at the control radius would make them read as buttons.
The guide's marketing radii are larger — 16 pt cards, 12 pt buttons, 10 pt inputs, pill for chips — and that is correct for a landing page. A dense product row at 12 px reads as a row of pills. Do not introduce a third product step in either direction.
Shadow — soft and warm, never blue-grey
| State | Value | Token |
|---|---|---|
Card resting | 0 1px 2px rgba(0,0,0,.04), 0 1px 6px rgba(0,0,0,.04) | --shadow-card |
Card hover | 0 4px 12px rgba(0,0,0,.06), 0 12px 32px rgba(0,0,0,.06) | --shadow-card-hover |
Modal / overlay | 0 24px 80px rgba(0,0,0,.18) | --shadow-modal |
A resting surface is a LINE, not a shadow. Cards sit on a 1px border; the shadow is barely there. Reach for elevation only when something genuinely floats (a popover, a dialog, a map panel).
Reach them as shadow-[var(--shadow-card)], or the minted shadow-overlay /
shadow-modal utilities. See 11-traps.md for why --shadow-card cannot be
re-declared inside @theme.