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

ModeUse it for
comfortableThe default: forms-first pages, marketing-adjacent screens, anything touch-operated. Soma's airy default rhythm.
cosyThe in-between: mixed dashboards where widgets and tables share the viewport and a little more fits without feeling cramped.
compactDense operator consoles: long tables, log-heavy monitoring views, screens read at a distance from a mouse.

Modes

What each mode sets, in pixels:

ValueControl / row / block padding
(unset) / comfortable7 / 10 / 16 px — the default.
cosy5 / 8 / 12 px.
compact4 / 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.

Density sample

A table, buttons and a field at the current density. Switch modes above and watch rows, controls and widget padding tighten while the type scale stays put:

NeuronStatusRuns
ingest-apiactive128
plan-reviewpending36
batch-exportfailed7

Tokens

The whole mechanism is three custom properties, restated by the cosy and compact attribute blocks in _tokens.scss:

TokenWhat consumes it
--soma-density-control-yVertical padding of controls: buttons, inputs, selects, chips, segmented controls.
--soma-density-cell-yRows: table cells, list-group and key-value rows, menu items, sidebar links, tree rows, palette rows.
--soma-density-block-yBlock 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 {}
});