FSAI Design System
Foundations

Control Heights

The four-rung height scale every control sits on, and why badges deliberately do not.

Overview

Two sizing rules live side by side in the system, and knowing which one a component follows is the whole point of this page.

  • A control pins its height. Buttons, inputs, selects, icon buttons, popover triggers, segmented tracks. The height is the design decision — "toolbar controls are 32px" — and the label sits inside it.
  • A badge derives its height. Badges, chips, status pills, tags. The label is the content, so height falls out of type token + padding + border, and grows with the type scale.

Mixing the two is what makes a panel header look ragged: a status pill sitting 8px proud of the icon button beside it, a segmented control 4px taller than its neighbouring button.

The scale

Anything that occupies a full row slot beside another control sits on one of four rungs:

RungTokenHeightTypical use
xs--control-h-xs28pxDense table toolbars, inline row actions
sm--control-h-sm32pxPanel headers, filter bars, secondary chrome
md--control-h-md36pxDefault form controls, page-level actions
lg--control-h-lg40pxModal CTAs, prominent actions

The tokens are declared in packages/config-tailwind/theme.css and consumed as a Tailwind arbitrary property:

// ✓ consume the rung
<div className="h-(--control-h-sm)" />

// ✗ never re-declare the raw pixel — this silently drifts when the scale moves
<div className="h-8" />

Which components sit on it

ComponentRungs it uses
Buttonxs sm md lg
IconButtonxs sm md lg — square, width comes from the same rung
Inputsm md
Selectxs sm md
Popover triggerxs sm md lg
ButtonGroup / SegmentedControlxs sm md — the track is on the scale, not the buttons inside it

ButtonGroup is worth calling out: the track carries the height and its buttons fill it with h-full. Sizing the buttons and letting the track add its padding on top is what made a segmented control 4px taller than a lone Button of the same size.

Why controls pin and badges don't

Left to derive, a single size tier resolves five different ways:

All at sm, derivedHeight
Button, primary (no border)34px
Button, secondary (border)36px
Input (text-sm)30px
Select (text-md)32px
Popover trigger32px

A bordered variant comes out 2px taller than a borderless one, and components at the same tier reach for different type tokens. Pinning also lets IconButton be genuinely square, which cannot be derived from a 16px glyph.

Badges go the other way, because the two surfaces run different typography:

Badge heightsmmdlg
Brand dashboard19px24px30px
Franchisee portal20px26px32px

The franchisee portal runs larger type; a badge frozen at a brand-dashboard pixel would crowd its own label there. Because every badge-shaped thing runs one shared recipe, badges agree with each other on any given surface without having to agree on a number. Controls hold their rung on both.

This split is not ours alone — Material 3 (chip 32dp / button 40dp), MUI (32 / ~36), Ant (tag ~22 / button 32), Carbon (24 / 40) and Primer (~20 / 32) all size tags independently of buttons.

Count pills

Count pills are the exception in the other direction. They are circles containing a number, so they need a fixed square box rather than either rule:

<span className="size-(--count-pill-size)" /> // 20px

Chips do not belong here — they are text-shaped and derive their height.

How it is checked

A vitest helper resolves rendered Tailwind classes against the parsed theme.css and does the arithmetic — no browser. It throws on any class it cannot resolve rather than guessing.

  • Controls are asserted against their rung under both typography profiles.
  • Badges are asserted to agree with each other and to grow with the type scale, rather than to hit a pixel. Asserting a pixel would reintroduce the coupling the shared recipe removes.

Storybook → Foundations / Control Heights renders all four tiers on a ruled background.

Guidelines

  • Do consume --control-h-* on anything that takes a full slot in a row
  • Do size a ButtonGroup by its track, not its buttons
  • Do let badges, chips, tags and status pills derive their height
  • Don't pin a height on a badge-shaped element — it must respond to the type scale
  • Don't re-declare a raw h-8 / h-9 on a control
  • Don't reach for a rung to line a badge up with a button; they are not meant to match

On this page