Density
Summary
Soma ships three spacing modes:
comfortable (the default),
cosy and compact, switched at
runtime by the data-soma-density attribute on
<html> or any subtree. A mode changes
vertical padding on controls, rows and blocks, never the type
scale, so text stays equally readable in every mode. There is
nothing to rebuild and no alternate stylesheet to load: each
mode restates three --soma-density-* tokens and
every component follows.
Density is one of Soma's runtime presentation axes — the others are theme (light / dark / high-contrast, plus direction/RTL) and strings. The corner buttons on any sandbox page cycle all of them.
When to use
| Mode | Use it for |
|---|---|
comfortable | The default: forms-first pages, marketing-adjacent screens, anything touch-operated. Soma's airy default rhythm. |
cosy | The in-between: mixed dashboards where widgets and tables share the viewport and a little more fits without feeling cramped. |
compact | Dense operator consoles: long tables, log-heavy monitoring views, screens read at a distance from a mouse. |
Modes
What each mode sets, in pixels:
| Value | Control / row / block padding |
|---|---|
(unset) / comfortable | 7 / 10 / 16 px — the default. |
cosy | 5 / 8 / 12 px. |
compact | 4 / 6 / 8 px — for dense operator consoles. |
Example
Pick a mode and the whole sandbox re-spaces live; the choice sticks across pages while you browse (same store the corner cycler uses). Unlike theme switches, density swaps plainly, with no view transition.
Tokens
The whole mechanism is three custom properties, restated by the
cosy and compact attribute blocks in
_tokens.scss:
| Token | What consumes it |
|---|---|
--soma-density-control-y | Vertical padding of controls: buttons, inputs, selects, chips, segmented controls. |
--soma-density-cell-y | Rows: table cells, list-group and key-value rows, menu items, sidebar links, tree rows, palette rows. |
--soma-density-block-y | Block containers: widget and panel padding. |
The tokens are consumed, never set: component
CSS reads them and only _tokens.scss assigns them,
the same discipline as theme tokens. Consume them for your own
components so they follow the mode too; add
calc() offsets rather than fresh literals when a
component needs a different rhythm at the same density. The
type scale is deliberately not density-aware — modes change
rhythm, not readability. Live resolved values are on the
Design tokens page.
Target sizes
Compact stays honest to WCAG 2.2 §2.5.8 (Target Size, Minimum):
interactive elements whose padding could shrink below the line
carry min-block-size: 24px guards (filter chips,
date-picker day cells and footer buttons, tree rows, the
switch, checkboxes and radios), so every pointer target keeps
at least 24 px even in the densest mode. If you build
custom controls on the density tokens, keep the same floor.
HTML
Density is purely declarative. Set the attribute on the root, or on any subtree (tokens cascade, so everything inside follows):
<html data-soma-density="compact"> <!-- whole page -->
<div data-soma-density="compact"> <!-- just this subtree, e.g. one dense table -->
…
</div>
JavaScript
There is no JS helper. Density is the attribute, nothing else
(unknown values simply fall back to comfortable, the
:root defaults):
// Switch — removing the attribute restores the comfortable default.
document.documentElement.setAttribute('data-soma-density', 'compact');
document.documentElement.removeAttribute('data-soma-density');
// Read — null/absent means comfortable.
const mode = document.documentElement.getAttribute('data-soma-density') || 'comfortable';
The switcher on this page persists the choice under the same
localStorage key the sandbox's corner cycler uses,
so it follows you to every other page; an app would do the
same with its own preference store:
const DENSITY_KEY = 'soma-sandbox-density'; // the corner cycler's store
button.addEventListener('click', () => {
document.documentElement.setAttribute('data-soma-density', button.dataset.density);
try { localStorage.setItem(DENSITY_KEY, button.dataset.density); } catch {}
});