Spacing & density

How space groups things, controls line up, the compact scale and tighter rows, and what corners say.

On this page

Usage

import { Button, Column, Surface, Table, Text, TextField, Theme } from '@envsemble/envui';

// Padding is inside the surface; gap is between its children.
<Surface padding={5} render={<Column gap={4} />}>
  <Text render={<h2 />} variant="title">
    Workspace
  </Text>
  <Text>Shared plans and notes for the project.</Text>
</Surface>;

// A desktop productivity app: one compact scale for the whole window, and tighter rows.
<Theme theme="light" scale="compact">
  <TextField label="Title" defaultValue="Quarterly report" />
  <Button>Rename</Button>
  <Table.Root aria-label="Reports" density="sm">
    …
  </Table.Root>
</Theme>;
PropTakes
gap, paddingSpacing steps 0–9 on Row, Column, Grid, Box and Surface; 0 to 4 rem in the comfortable scale, smaller in compact
Theme scalesystem, comfortable or compact; inherited from the enclosing Theme, system outside any; written to data-env-scale
ThemePreferenceProvider scaleAs on Theme, for its boundary and the document root; left out, it inherits <html data-env-scale>
createThemeBootstrapScriptscale: 'compact' or 'comfortable' writes data-env-scale to the root before the first paint; system writes nothing
Control sizesm, md or lg on Button, IconButton, the fields, Select, Combobox, DatePicker, SegmentedControl, Pagination and the rail of Tabs
Row densityxs, sm, md or lg on Table.Root, Tree and List.Root: only the rows' block padding changes
Touch minimum2.75 rem: every control grows to it under a coarse pointer

The words of the figures

  • Tight, inside a group: gap 1–3: a heading and its description, a label and its value, short lines that read as one block.
  • Roomier, between groups: gap 4–5: the fields of a form, a form and its actions, the groups of a section.
  • The most, between sections: gap 6–9: the sections of a page or a settings pane.
  • Components bring their own padding: don't pad a Button, field or Card from outside; arrange the space between them.
  • Big enough for a finger: nothing to do; controls grow to the touch minimum under a coarse pointer, as Touch says.

Composition

Two axes of density

  • Scale is the size of the whole system, comfortable or compact: it switches every size token at once. Compact is about 0.8 of comfortable in space and corners, with controls a step shorter and type a step smaller, for desktop productivity apps. Icons keep a 1 rem floor; borders and focus rings keep their widths. system, the default, is compact under pointer: fine and comfortable otherwise; the stylesheet decides, so the server render and the first paint already have it.
  • Row density is how tightly Table, Tree and List pack their rows: xs to lg, padding only.

Within a scale, size picks one of three control heights for a deliberate local difference; every control of one size lines up. A control without a size takes its container's --env-control-height, md unless the container sets another, as a table cell does with sm.

Guidelines

  • The parent owns gaps, through Row, Column and Grid; each component owns its padding. Text has no outer margins.
  • Use relative units; let text wrap and controls grow with their content. In your own grids, minmax(0, 1fr) tracks, min-inline-size: 0 on flex children and max-inline-size: 100% on media.
  • Choose Surface for a plain pane and Card for a content surface, and keep their corners.
  • Use --env-radius-full where a pill is meant, never 50% or 9999px: one line tall it draws round ends, and an element that grows taller, such as a wrapping button, keeps the same corners. 50% is only for parts that are always square, such as avatars and round icon buttons.
  • One scale per app or window, on the root Theme or ThemePreferenceProvider. Leave it at system for a tool used with a mouse and on touch; set comfortable for a website with room to breathe, or compact for a dense tool on touch too. A fixed scale, rendered on the server, goes to the provider and to createThemeBootstrapScript too, so the first paint has it.
  • Another scale only at a Theme boundary around a whole region, such as a docked inspector: <Theme {...useTheme()} scale="compact">.
  • Rows match: a button beside a field takes its size, and a table's toolbar follows its rows.
  • Choose a row density for how people read: sm or xs for scanning, md or lg for reading row by row.
  • Read the tokens in application CSS, such as block-size: var(--env-control-height-sm).

Customize

Inside a Theme these tokens follow the theme, its scale and the reader's text size. Their values in each scale are under Sizes in each scale, and every value is on the Design tokens page of the Docs.

TokenHolds
--env-space-1 … --env-space-9The spacing scale
--env-radius-xs0.25 rem: small marks, checkboxes and color swatches
--env-radius-smInner rows, important actions and thumbnails
--env-radius-lgAlerts, toasts, fields, text areas, tables and drop zones; a single-line field's corner is no more than a third of its height
--env-radius-surfacePanes, cards, dialogs, sheets and the outside of a row group
--env-radius-fullRounded ends: buttons, navigation items, badges; half the tallest single-line control, 1.5 rem at most, so a taller element is a rounded rectangle
--env-control-height, --env-control-height-sm / -md / -lgControl heights, to line up with
--env-control-icon-size1 rem: the icon inside controls

Corners by what they are

The catalogue shows four kinds of corner; small marks are a fifth, for code. Their tokens:

In the catalogueToken and where it is used
Surfaces--env-radius-surface: panes, cards, dialogs, sheets and the outside of a row group; popups take a smaller corner, concentric with their rows
Inside a surface--env-radius-lg for alerts, toasts, fields, tables, text areas and drop zones; --env-radius-sm for list rows, menu items, tabs and tooltips
Fully rounded--env-radius-full: buttons, badges, switches, segmented controls and navigation items
Circles50%: avatars and icon buttons, which are always square
Small marks--env-radius-xs: checkboxes and color swatches, which read as boxes

Sizes in each scale

At the default 16 px root; every value is a token in rem.

TokenComfortableCompact
--env-control-height-sm / -md / -lg32 / 40 / 4828 / 36 / 44
--env-row-height (calendar days; popup rows are 0.25 rem more)3628
--env-row-padding-block-xs / -sm / -md / -lg2 / 4 / 8 / 120 / 2 / 4 / 6
--env-space-1 … --env-space-94, 8, 12, 16, 24 … 644, 6, 10, 12, 20 … 52
--env-font-size-caption / -body-sm / -label12 / 14 / 1411 / 12 / 13
--env-font-size-body / -title / -headline15 / 18 / 2213 / 16 / 20
--env-icon-size-sm / -md / -lg16 / 18 / 2016 / 16 / 18
--env-picture-size-sm / -md / -lg / -xl (Avatar, Graphic)24 / 32 / 48 / 8824 / 32 / 48 / 88
--env-radius-sm / --env-radius-lg / --env-radius-surface8 / 16 / 286 / 12 / 22
--env-top-bar-height / --env-title-bar-height52 / 4042 / 32
--env-control-target-min / --env-control-height-touch24 / 4424 / 44

Touch

  • Under a coarse pointer, @media (pointer: coarse), controls and rows grow to at least 44 px in either scale, and entered text becomes 1 rem so phones don't zoom into a field. A touchscreen laptop used with a mouse keeps the desktop density.
  • A control's own box is its hit area, so a press always reaches the control it lands on.
  • Grow your own targets the same way, since --env-control-height keeps its value under touch:
.my-target {
  block-size: var(--env-control-height);
}
@media (pointer: coarse) {
  .my-target {
    block-size: max(var(--env-control-height), var(--env-control-height-touch));
  }
}
  • A link in text, a breadcrumb and a Switch's track stay smaller, under WCAG 2.5.8's spacing exception; with a mouse every target is at least 24 px.

TypeScript declarations provide the complete public API.

Words in code

WordsIn code
More room between theseThe next larger step of the gap that parts them, on their parent.
Group these closerWrap them in their own Column or Row with a smaller gap, inside the larger one.
Separate sectionsA larger gap between them, never a line.
Line these upOne size for every control in the row, and align="center" on the Row.
A small buttonsize="sm", "md" (the default) or "lg" on a control.
Compactscale="compact" on the root Theme: every control height, gap, type size and corner a step smaller.
Comfortablescale="comfortable" on the root Theme.
Follow the devicescale="system", the default: compact under a fine pointer, comfortable on touch.
Tighter rowsdensity="sm" or "xs" on a Table, List or Tree; the text keeps its size.
On a surfaceSurface for a plain pane, Card for a content surface; both keep --env-radius-surface.
Fully rounded--env-radius-full, as Composition says.