Sizing

The semantic token scales, and the precedence ladder an icon's size resolves through.

Sizes are semantic tokens, not raw utilities. tokens.css owns the values in Tailwind's own namespaces, so they compile to ordinary utilities.

TokenUtilityWhat it sizes
--spacing-button-*h-button-md, w-button-mda button's height, and its square footprint at an icon-* size
--spacing-icon-*size-icon-mdany glyph — Icon, Spinner, a row's chevron
--text-button-*text-button-mda button's label, paired with its height
--radius-button-*rounded-button-mda button's corner, half its height — a capsule, and a circle at an icon-* size
--spacing-input-*h-input-md, min-h-input-mda field's height — fixed on one line, a floor when multiline
--text-input-*text-input-mda field's value, paired with its height
--spacing-navbar-rowh-navbar-rowthe navbar's control row, without its safe-area band
--spacing-screen-gutterpx-screen-gutterthe gutter Screen.Header, Screen.Navbar and content share

Corners

Every generic rounded-* step is a multiple of one --radius, shadcn's own scale — so a --radius copied out of a web app retunes every rounded edge in the library at once.

StepValue
rounded-xs--radius × 0.4
rounded-sm× 0.6
rounded-md× 0.8
rounded-lg--radius
rounded-xl× 1.4
rounded-2xl× 1.8
rounded-3xl× 2.2
rounded-4xl× 2.6

Multipliers rather than the calc(var(--radius) - 4px) form some generators still emit: a multiplier scales, so a square-cornered theme stays square at every step instead of going negative at the bottom and staying rounded at the top.

A shape is a token too, and --radius-button-* is deliberately not part of that scale. It is half each button height, so a button is a capsule and a square one a circle whatever --radius a consumer sets — the default, not a law: change those three numbers for a squarer kit, or pass className="rounded-lg" to square off one button. A radius above half the height is clamped by the renderer, and a test keeps the tokens under it.

--radius is the only step that survives to runtime. The scale is declared @theme inline, so Tailwind substitutes each step into the utilities that use it and emits no --radius-md variable — anything computing a corner in JavaScript reads --radius and applies the multiplier itself, the way Checkbox does for the fill that has to stay concentric with its border.

A token is named for what it sizes, not for an abstract category. A control that shares another's heights gets its own namespace rather than borrowing one — --spacing-input-* names the same 36/44/52 as --spacing-button-* so a field and the button beside it can be retuned independently. They are meant to stay level, and a test asserts that outright.

Icon and Spinner share one scale

SPINNER_SIZES is ICON_SIZES — xs through 2xl, 14/16/18/20/24/32pt — so size="md" is the same edge length in both and one can stand in for the other with nothing moving. That is what makes a button's loading swap free: both glyphs are drawn at the button's own size-icon-* token, so replacing one with the other costs no layout.

A component indexes that scale at its own step name rather than restating a number. A Checkbox's square reads it two steps above its own tick — 18/14, 20/16, 24/18 — and the test pins the offset rather than the points, so the icon scale can be retuned without the test becoming a transcript of it.

An icon's size is a class that drives a prop

react-native-svg lets a glyph's own width/height win over any style, so a size-* that resolved to style.width would be overridden every time. Icon recovers the width from the class and hands it to the glyph as a number instead — which is why both spellings work, and why they resolve in the order below.

Precedence, weakest first

The first four are one cn() chain, so the last size-* wins. The fifth is a different mechanism entirely, which is what makes "inherited class plus caller-supplied number" resolve correctly.

SourceHow it wins
ICON_FALLBACK_SIZE_CLASSfirst in the chain
the enclosing IconDefaultsProvider's classNamesecond
a named size="lg"third
the caller's classNamelast in the chain
a numeric size={18}bypasses the chain — the manual mapping is skipped when the target prop is already defined

On this page