Foundations
Density
Density is an attribute of a container. One attribute on a panel retunes everything inside it: the tier of roles changes, and the components hold no numbers of their own.
Density moves geometry and does not touch type size: in all three modes the text stays 14px, and what changes is the air around it. Size is the business of the second axis — scale — and the axes add up.
Markup
<div class="inst-cluster" data-density="compact">
<button class="inst-btn inst-btn--sm" type="button">sm</button>
<button class="inst-btn" type="button">md</button>
<span class="inst-badge" data-tone="ok"><span class="inst-dot"></span>done</span>
<input class="inst-input inst-input--sm" type="text" value="compact">
</div>
Contract
<section data-density="compact">
<!-- everything inside is retuned -->
</section>
The attribute is inherited through the cascade of custom properties, so it works at any depth and on any container. Nesting is allowed: an inner region with a different value overrides the outer one.
| Value | What for |
|---|---|
| no attribute | The default. A panel tool, a dashboard, most screens |
data-density="compact" |
A dense tabular view: an agent queue, a log, a tree, a property inspector |
data-density="comfortable" |
Forms, settings, wizards — where there are few decisions and each wants attention. Also a conversation: a chat is read rather than scanned, and the taller control is the one that matches it |
Accessibility
Tap targets
WCAG 2.5.8 (AA) asks for a tap target no smaller than 24×24 CSS pixels. At
density compact a control of size sm gives 22px — below the threshold.
That is a deliberate trade: a dense tabular view exists for the sake of
density.
| Density | --control-h-sm |
--control-h-md |
The 24px threshold |
|---|---|---|---|
compact |
22px | 26px | sm does not pass |
| default | 26px | 32px | Passes |
comfortable |
30px | 36px | Passes |
The rule of application: compact is for dense tabular views, not for the
main navigation and not for the principal actions of a screen. If a user hits
this control with a mouse every day, it must not be 22px. The WCAG exception
for targets that have an equivalent of a normal size applies only when such an
equivalent really exists.
Everything else
| Type size | It does not shrink at any density: the floor of 11px stays the floor |
| Contrast | It does not depend on density — the same pairs are checked once |
| Text zoom | The heights are applied as min-block-size, so an increased browser font stretches a control rather than cutting it off. True at all three densities |
| Keyboard | Density changes neither the order of traversal nor the targets it hides: it is pure geometry |
Scale
Exactly the tier of roles, and that is enough.
Control heights and paddings
| Token | compact |
default | comfortable |
|---|---|---|---|
--control-h-xs |
18px | 20px | 22px |
--control-h-sm |
22px | 24px | 28px |
--control-h-md |
26px | 32px | 36px |
--control-h-lg |
32px | 38px | 44px |
--control-pad-sm |
--space-3 |
--space-4 |
--space-5 |
--control-pad-md |
--space-4 |
--space-5 |
--space-6 |
--control-pad-lg |
--space-5 |
--space-6 |
--space-7 |
Region paddings
| Token | compact |
default | comfortable |
|---|---|---|---|
--pad-panel |
--space-4 |
--space-5 |
--space-6 |
--pad-card |
--space-5 |
--space-6 |
--space-7 |
--pad-cell-x |
--space-3 |
--space-4 |
--space-5 |
--row-pad-y |
--space-2 |
--space-3 |
--space-4 |
--gap-row |
--space-2 |
--space-3 |
--space-4 |
--gap-inline |
--space-3 |
--space-4 |
--space-5 |
Glyphs and gutters
| Token | compact |
default | comfortable |
|---|---|---|---|
--size-check |
13px | 15px | 17px |
--size-switch-w |
26px | 30px | 34px |
--size-switch-h |
15px | 18px | 20px |
--size-thumb |
12px | 14px | 16px |
--size-spinner |
13px | 14px | 16px |
--size-gutter |
12px | 14px | 16px |
--size-indent |
12px | 14px | 16px |
What is NOT retuned
| What | Why |
|---|---|
| Type size | Density is about geometry. Shrinking the text along with the paddings means hitting the 11px floor within one step. Type size is moved by scale — the second axis, which adds up with this one |
| Colour and contrast | The thresholds do not depend on how many rows fit on a screen |
| Radii | The shape of an object does not change with how tightly it stands |
--size-dot, --size-marker, --size-icon |
Carriers of meaning. A dot at 4px does not read, a mark at 1px is not seen |
--space-* |
The scale is the foundation. Density moves the roles, not the scale under them |
Behavior
The tier of roles is declared at zero specificity: a default with the same specificity as an override stops being a default.
So an application overrides any role without a fight — including those density does not retune:
.my-inspector { --label-col: 120px; }
Rules
A component that breaks at compact holds a hard-coded constant that ought to
be a role. Three densities are an obligatory run when a component is added, on
a par with the contrast check.
What to take and what not to
| Use | Take instead |
|---|---|
compact for a dense tabular view: a queue, a log, an inspector |
compact for the main navigation — the tap targets go below the threshold, see the warning and the shell |
comfortable for a form and for settings filled in thoughtfully |
comfortable for a long list — half as many rows fit on a screen as a queue row needs |
| The attribute on a container — a panel, a section, a dialog | The attribute on every component — density is not a variant. The size of a control is chosen by --sm/--lg on the button |
| The default — writing nothing | A density of your own as a number in an application — a new value gets a role, and the list of roles is in tokens |
| Changing the density of a whole region | Mixing densities in one table — the columns will part, see the table |
API
| Name | Value | What it does |
|---|---|---|
| attribute | ||
data-density | — | compact comfortable. The absence of the attribute is the third, base value |
| Name | Value | Where from |
|---|---|---|
| token | ||
--control-h-xs/sm/md/lg | — | one across every cell |
--control-pad-sm/md/lg | — | one across every cell |
--pad-panel | var(--space-5) | base +14: density · scale |
--pad-card | var(--space-6) | base +14: density · scale |
--pad-cell-x | var(--space-4) | base +14: density · scale |
--row-pad-y | var(--space-3) | base +14: density · scale |
--gap-row | var(--space-3) | base +14: density · scale |
--gap-inline | var(--space-4) | base +14: density · scale |
--size-check | 15px | base +14: density · scale |
--size-switch-w | 30px | base +14: density · scale |
--size-switch-h | 18px | base +14: density · scale |
--size-thumb | 14px | base +14: density · scale |
--size-spinner | 14px | base +14: density · scale |
--size-gutter | 14px | base +14: density · scale |
--size-indent | 14px | base +14: density · scale |
--label-col | 104px | base +4: scale |
Why the kit works this way — the constitution · Open source under MIT, github.com/keshon/instrument