All documents

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:

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

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

ElementClassMeasured
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

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