Skip to content
RU
Scale
Density
Theme
Accent

Foundations

Rhythm and shape

A step of 4px, and the scale is deliberately sparse at the top — so that "a little bigger" is simply not in the set. A component takes its padding from the tier of roles rather than from the scale: that is what gives density one point through which it retunes everything at once.

The component
worldgen-01
Generating biomes, 3 passes
done
Markup
<div class="inst-card inst-stack inst-stack--tight">
  <div class="inst-card-title">worldgen-01</div>
  <div class="inst-card-sub">Generating biomes, 3 passes</div>
  <div class="inst-cluster">
    <button class="inst-btn inst-btn--sm" type="button">Retry</button>
    <span class="inst-badge" data-tone="ok"><span class="inst-dot"></span>done</span>
  </div>
</div>

Contract

A padding is taken by purpose: --pad-panel, not --space-5. A role is retuned by density, a step of the scale is not.

.my-panel {
  padding: var(--pad-panel);
  gap: var(--gap-row);
  border-radius: var(--radius-lg);
}
What Required Why
A role rather than a step yes --pad-panel changes along with density, --space-5 does not. A component with a hard-coded step is the first to break
The gap is set by the container yes A margin on an element adds up with the parent's inner padding vertically and does not add up horizontally
A radius from the vocabulary yes Five steps named by the size of a control. A number of your own parts from its neighbours at the joint
A hairline through --hairline yes At a fractional DPR that is 0.5px, and the line stays a line rather than a band

Accessibility

Tap targets The padding of a control takes part in the size of the target on a par with the height. At compact small controls go below 24px under WCAG 2.5.8 — see density
Geometry in px Paddings and radii do not grow along with type size: a dense tooling look would otherwise fall apart. The text grows, and the heights work as a minimum
Parting groups Rhythm is a carrier of meaning too: four metrics with no borders read as one group precisely by the gap
The hairline On a 1x display it stays 1px: a line that has to be visible is visible on any screen

Scale

A component reaches it only through the tier of roles. Using --space-* directly is allowed in an application and in the flow primitives.

Token Value
--space-1 2px
--space-2 4px
--space-3 6px
--space-4 8px
--space-5 12px
--space-6 16px
--space-7 24px
--space-8 32px
--space-9 48px
--space-10 64px

The sparseness at the top is not thrift but a restriction: there is no step between 32 and 48, and that is right. A step that does not exist cannot be chosen by mistake.

Paddings by purpose

The tier of roles. The values below are for the default density; at compact and comfortable they differ, see density.

Token Default Where
--pad-panel --space-5 The inner padding of a panel
--pad-card --space-6 The inner padding of a card
--pad-cell-x --space-4 The horizontal padding of a table cell
--row-pad-y --space-3 The vertical padding of a row: a task, a cell, a menu item
--gap-row --space-3 The gap between rows in a stack
--gap-inline --space-4 The gap between neighbours in a row
--control-pad-sm --space-4 The horizontal padding of a small control
--control-pad-md --space-5 The same for a medium one
--control-pad-lg --space-6 The same for a large one

The gap is set between neighbours. A margin-block on every element adds up with the container's inner padding at the top and the bottom but not at the sides — and a highlight steps away from the border twice as far vertically as horizontally. The right form is X + X { margin-block-start }.

The sizes of glyphs and gutters

The same tier of roles: every size has a name, so there are no constants in the components.

Token Default What it is
--size-dot 6px The state dot
--size-check 15px The checkbox and the radio
--size-switch-w / --size-switch-h 30 / 18px The track of a switch
--size-thumb 14px The thumb
--size-track 4px The track of a slider
--size-meter 6px The thickness of a meter
--size-spinner 14px The ring of busyness
--size-icon 18px An icon — see icons
--size-chevron 10px A chevron, a small icon
--size-marker 2px A load-bearing mark: a tab's bar, the edge of a note, the edge of the current navigation item. Thinner and it is not seen, thicker and it argues with the text
--size-gutter 14px The left gutter of a task row
--size-indent 14px The indent step of a tree
--label-col 104px The label column of the inspector

Radii

Six steps, and what chooses among them is nesting. All the values are even: at a density of 1.5 an odd radius rasterises off the device grid.

Token Value What for
--radius-2xs 2px The bottom step. Marks reach it through --radius-mark rather than by name
--radius-xs 4px Inline: badges, chips, a key
--radius-sm 6px Controls inside a card or a panel
--radius-md 8px Free-standing controls: a button, a field, a popover
--radius-lg 12px Cards, panels, the modal
--radius-full 999px Tracks, thumbs, the avatar

The rounding of a CONTROL is taken not from here directly but from --radius-control-sm/md/lg: it travels with density along with the height, otherwise a small button reads as a pill. See density.

The rounding of a MARK is taken from --radius-mark for a reason of the opposite kind. Below 8px the ladder stops offering a choice: when two radii on one edge add up to more than the edge is long, the browser scales every radius on the box until they fit, silently. --radius-xs on a 6px dot arrives as 3 — half the box, which is a circle; on a 6px meter it arrives as 3 as well, which is a pill. And where the other side of the box is data — the width of a lane span, the width of a tick — the shape then changes with the data. The mark tier is the one step that survives every mark in every scale.

The rule of nested radii

An inner corner needs a radius smaller than the outer one, otherwise the inner element looks as if it bulges out of its container. Optically the inner radius equals the outer minus the thickness of the gap between them, and the scale is already chosen so that the neighbouring step down gives the right result at a padding from the tier of roles.

<!-- panel lg → a control inside sm; a free-standing control is md -->
<div class="inst-panel">
  <div class="inst-panel-body">
    <div class="inst-card"></div>
  </div>
</div>

A rounding on a one-sided border is always a bug. A border-left together with a border-radius gives a stump of an arc: a note and the edge of the current navigation item carry a square corner deliberately.

The hairline

A browser never draws a border thinner than one physical pixel. On a 1x display 0.5px and 1px give the same thing, while a negative margin of -0.5px against a border drawn at 1px leaves half a pixel of doubled line. So the hairline is switched on only where there is room for it.

Token Value When
--hairline 1px A pixel density below 1.5dppx
--hairline 0.5px @media (min-resolution: 1.5dppx)
--stroke 1px A line that has to stay visible always

The seam of a button group is made by removing the neighbour's border — for the same reason.

Rules

Token Value What it is
--aside-w 232px The side column of the shell
--container-max 76rem The ceiling on the width of the working area. In rem, because it is a measure of readability rather than geometry
--split-side 18rem The wanted width of a split's side part
--split-main 30rem Below this the main part wraps underneath
--col-min 260px The minimum column of a responsive grid

What to take and what not to

Use Take instead
A role: --pad-panel, --gap-row, --row-pad-y --space-* in a component — density retunes the tier of roles, not the scale. The whole list of roles is in tokens
A gap between neighbours — a flow primitive .inst-stack / .inst-cluster / .inst-grid A spacing utility (mt-3, p-2) — the library has none and will have none, see flow
A step of the gap by name: ordinary, --tight, --loose A gap of your own as a number — "a little bigger" must not be an available decision. The three steps are described in flow
A radius by nesting: a container lg, a control inside sm One radius for everything — an inner element with the container's radius bulges out. An example is the card
--hairline for a line that must not shout 1px as a number — on a 2x display that is twice the wanted thickness, see the hairline

API

Classes
NameWhat it does
class
inst-stackA vertical flow, gap --gap-inline
inst-clusterA horizontal flow that wraps
inst-cluster-spacerA break in a row: everything after it goes to the right
inst-gridA responsive auto-fit grid by --col-min
modifier
inst-stack--tightA step of the gap
inst-stack--looseA step of the gap
inst-cluster--tightA step of the gap
inst-cluster--looseA step of the gap
inst-grid--tightA different minimum column
inst-grid--wideA different minimum column
Tokens
NameValueWhere from
token
--space-1A step of the scale2pxone across every cell
--space-2A step of the scale4pxone across every cell
--space-3A step of the scale6pxone across every cell
--space-4A step of the scale8pxone across every cell
--space-5A step of the scale12pxone across every cell
--space-6A step of the scale16pxone across every cell
--space-7A step of the scale24pxone across every cell
--space-8A step of the scale32pxone across every cell
--space-9A step of the scale48pxone across every cell
--space-10A step of the scale64pxone across every cell
--pad-panelThe inner padding of a panelvar(--space-5)base +14: density · scale
--pad-cardThe inner padding of a cardvar(--space-6)base +14: density · scale
--pad-cell-xThe horizontal padding of a cellvar(--space-4)base +14: density · scale
--row-pad-yThe vertical padding of a rowvar(--space-3)base +14: density · scale
--gap-rowThe gap between rowsvar(--space-3)base +14: density · scale
--gap-inlineThe gap in a rowvar(--space-4)base +14: density · scale
--control-pad-smThe padding of a small controlcalc(var(--control-h-sm) * var(--control-ratio-sm))one across every cell
--control-pad-mdThe padding of a base controlcalc(var(--control-h-md) * var(--control-ratio-md))one across every cell
--control-pad-lgThe padding of a large controlcalc(var(--control-h-lg) * var(--control-ratio-lg))one across every cell
--radius-2xsThe bottom step of the ladder. Marks take it through --radius-mark2pxbase +4: scale
--radius-markThe rounding of a mark: anything the kit draws below 8px — the dot, a tick, the meter and the lanevar(--radius-2xs)one across every cell
--radius-xsInline: badges, chips, a key4pxbase +4: scale
--radius-control-smThe rounding of a small control. It travels with density along with the heightvar(--radius-sm)base +14: density · scale
--radius-control-mdThe rounding of a controlvar(--radius-md)base +14: density · scale
--radius-control-lgThe rounding of a large controlvar(--radius-md)base +14: density · scale
--radius-smControls inside a card6pxbase +4: scale
--radius-mdFree-standing controls8pxbase +4: scale
--radius-lgCards and panels12pxbase +4: scale
--radius-fullA circle and a pill999pxone across every cell
--hairlineA hairline. At 1x it equals a pixel, above that a half1pxbase +1: screen
--strokeA line that has to stay visible always1pxone across every cell
--aside-wThe side column of the shell232pxbase +4: scale
--container-maxThe ceiling on the width of the working area76rembase +4: scale
--split-sideThe wanted width of a split's side part18rembase +4: scale
--split-mainThe threshold at which the main part wraps30rembase +4: scale
--col-minThe minimum column of a responsive grid260pxbase +4: scale
Classes
NameWhat it does
class
inst-stackA vertical flow, gap --gap-inline
inst-clusterA horizontal flow that wraps
inst-cluster-spacerA break in a row: everything after it goes to the right
inst-gridA responsive auto-fit grid by --col-min
modifier
inst-stack--tightA step of the gap
inst-stack--looseA step of the gap
inst-cluster--tightA step of the gap
inst-cluster--looseA step of the gap
inst-grid--tightA different minimum column
inst-grid--wideA different minimum column
Tokens
NameValueWhere from
token
--space-1A step of the scale2pxone across every cell
--space-2A step of the scale4pxone across every cell
--space-3A step of the scale6pxone across every cell
--space-4A step of the scale8pxone across every cell
--space-5A step of the scale12pxone across every cell
--space-6A step of the scale16pxone across every cell
--space-7A step of the scale24pxone across every cell
--space-8A step of the scale32pxone across every cell
--space-9A step of the scale48pxone across every cell
--space-10A step of the scale64pxone across every cell
--pad-panelThe inner padding of a panelvar(--space-5)base +14: density · scale
--pad-cardThe inner padding of a cardvar(--space-6)base +14: density · scale
--pad-cell-xThe horizontal padding of a cellvar(--space-4)base +14: density · scale
--row-pad-yThe vertical padding of a rowvar(--space-3)base +14: density · scale
--gap-rowThe gap between rowsvar(--space-3)base +14: density · scale
--gap-inlineThe gap in a rowvar(--space-4)base +14: density · scale
--control-pad-smThe padding of a small controlcalc(var(--control-h-sm) * var(--control-ratio-sm))one across every cell
--control-pad-mdThe padding of a base controlcalc(var(--control-h-md) * var(--control-ratio-md))one across every cell
--control-pad-lgThe padding of a large controlcalc(var(--control-h-lg) * var(--control-ratio-lg))one across every cell
--radius-2xsThe bottom step of the ladder. Marks take it through --radius-mark2pxbase +4: scale
--radius-markThe rounding of a mark: anything the kit draws below 8px — the dot, a tick, the meter and the lanevar(--radius-2xs)one across every cell
--radius-xsInline: badges, chips, a key4pxbase +4: scale
--radius-control-smThe rounding of a small control. It travels with density along with the heightvar(--radius-sm)base +14: density · scale
--radius-control-mdThe rounding of a controlvar(--radius-md)base +14: density · scale
--radius-control-lgThe rounding of a large controlvar(--radius-md)base +14: density · scale
--radius-smControls inside a card6pxbase +4: scale
--radius-mdFree-standing controls8pxbase +4: scale
--radius-lgCards and panels12pxbase +4: scale
--radius-fullA circle and a pill999pxone across every cell
--hairlineA hairline. At 1x it equals a pixel, above that a half1pxbase +1: screen
--strokeA line that has to stay visible always1pxone across every cell
--aside-wThe side column of the shell232pxbase +4: scale
--container-maxThe ceiling on the width of the working area76rembase +4: scale
--split-sideThe wanted width of a split's side part18rembase +4: scale
--split-mainThe threshold at which the main part wraps30rembase +4: scale
--col-minThe minimum column of a responsive grid260pxbase +4: scale

Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument