Skip to main content
View as Markdown

Density

One ladder of three steps — compact, cozy, roomy — each fixing the row padding, field height, small-button height, gutter and dense-text size that rows, cells and chips are drawn from.

How tight is a table row? Before this key the answer was py-[5px], written into a handful of Sovrium's own recipes and nowhere an author could reach. Density was not a token; it was a literal that happened to be the same in several files.

app.yaml
design:
  density:
    steps:
      compact: { rowY: 5px, controlH: 36px, buttonH: 28px, gap: 7px, text: 11px }
      cozy: { rowY: 8px, controlH: 40px, buttonH: 32px, gap: 10px, text: 13px }
      roomy: { rowY: 14px, controlH: 44px, buttonH: 36px, gap: 16px, text: 14px }

Those are the shipped values. Copying them changes nothing, which makes the block a useful starting point: edit one number and see exactly what it moves.

Path Kind Values Default Description
byZone object Which density step each declared zone runs at

steps

Path Kind Values Default Description
steps object The three named density steps, tightest first
steps.compact object (truncated) One step of the density ladder: row padding, field height, small-button height, affordance gap and dense text size
steps.cozy object (truncated) One step of the density ladder: row padding, field height, small-button height, affordance gap and dense text size
steps.roomy object (truncated) One step of the density ladder: row padding, field height, small-button height, affordance gap and dense text size

floors

Path Kind Values Default Description
floors object The accessibility floors a density step may never cross. Both values are fixed; the field exists so the guarantee is visible in the config document, not so it can be tuned.
floors.controlH enum 24 Minimum inline-control height in px. Fixed at 24 — declaring it restates the guarantee, and no other value decodes.
floors.hit enum 44 Minimum interactive hit-target size in px. Fixed at 44 — declaring it restates the guarantee, and no other value decodes.

The five numbers

Path Kind Values Default Description
rowY string Vertical padding inside a row or list item
controlH string Height of a text-entry control
buttonH string Height of a small button
gap string Horizontal gap between adjacent affordances
text string Font size of dense secondary text

Each one emits a custom property the recipes read: --sv-density-row-y, -control-h, -button-h, -gap and -text.

controlH and buttonH were one number until they were two, and the reason is worth a sentence. A field and a small button both sit on a line, but they are asked for different things: a field is a place to type, and wants room for a cursor, a descender and a comfortable target; a small button is a label with a box drawn round it, and wants to disappear into a toolbar. Under a single key you could not give the field room without inflating every button beside it. The shipped ladder puts them 8px apart at every step — fields 36 / 40 / 44, buttons 28 / 32 / 36 — and roomy's field lands exactly on the 44px enhanced target.

Every value is a number followed by px or rem. Nothing else decodes — no percentage, no clamp(), no unitless number. A step is a fixed, quotable number, and a fluid value is a different feature wearing the same name.

All five fields are required on every step, and all three steps are required. The step names are a closed set: cosy where you meant cozy is refused at boot, naming the key. A ladder missing a rung is not a ladder — a surface would silently fall back to a step you did not intend, and nothing would say so.

Which step you get

compact is the default. It is not merely first in the list: it is the step emitted on :root, so every surface renders at compact unless something has explicitly asked for another.

styles.css
:root {
  --sv-density-row-y: 5px; /* compact */
}
[data-density='cozy'] {
  --sv-density-row-y: 8px;
}
[data-density='roomy'] {
  --sv-density-row-y: 14px;
}

Anchoring compact at the root is deliberate. Its numbers are the literals the recipes used to hard-code, so an author who declares a ladder — even one copied verbatim from the block above — gets exactly what they had before. Anchoring cozy there instead would have silently loosened every existing table the moment anyone declared a density at all.

What moves today, and what does not

Three of the five properties are read by shipped recipes, so declaring a ladder changes real rendered padding:

  • rowY — the header cells and body rows of a table, bound or static.
  • text — the small in-cell affordances: a JSON preview, an array chip, an inline code span, a colour code, a barcode caption, a geolocation pair.
  • gap — the gutter inside a badge, and the chrome that reuses the badge's layout.

controlH and buttonH are emitted and read by nothing yet. Both custom properties carry your values; no recipe consumes either. Declare them — the schema requires both — but do not expect a field or a button to change height because of them. It is written down here rather than left to be rediscovered as a bug.

Not the same as the table's density control. A table toolbar can expose a density button letting a reader switch row height for themselves, and that preference is theirs and per-viewer. design.density is the app-level ladder those surfaces are drawn from. One is a runtime choice by whoever is looking; the other is a design decision by whoever wrote the config.

byZone — declared, not yet wired

byZone assigns a step to a zone, so a product surface can be tighter than a marketing one. It decodes and it is validated: a zone name design.zones does not declare is refused at boot, listing the zones that do exist.

app.yaml
design:
  zones:
    - { pattern: '/app/*', zone: product, accentBudget: product }
    - { pattern: 'everything else', zone: marketing, accentBudget: public }
  density:
    steps:
      compact: { rowY: 5px, controlH: 36px, buttonH: 28px, gap: 7px, text: 11px }
      cozy: { rowY: 8px, controlH: 40px, buttonH: 32px, gap: 10px, text: 13px }
      roomy: { rowY: 14px, controlH: 44px, buttonH: 36px, gap: 16px, text: 14px }
    byZone:
      product: compact
      marketing: roomy

It does not change anything yet. Nothing writes the [data-density] attribute onto a rendered element, so the cozy and roomy blocks are emitted and never matched. That is why all three steps are emitted regardless of what byZone says — it makes the remaining work a wiring change rather than a stylesheet change, and it means a ladder you declare today starts applying without you rewriting it.

floors

Two numbers, and only those two values decode — a lower floor is refused, and so is a higher one. Declaring the key restates a guarantee the engine already holds; omitting it is identical in every respect. It exists so the numbers are readable in a config rather than only in source, and it is honest to say nothing reads the key back.

What declaring one costs

Sovrium can serve an app-agnostic, precompiled stylesheet to apps that have customised nothing. Declaring design.density takes your app off that path: the ladder has to be compiled in, because serving the prebuilt file would leave every table on the platform's compact step while your config said otherwise. That is a first-request cost, not a per-request one, and it is the same trade any design customisation makes.

Behaviour

Data Table Interior Panel Theming Guarantees

  • Opening the filter overlay paints a non-transparent panel surface
  • Author primary token recolors the filter overlay Add-filter button
  • Opening the sort overlay paints a non-transparent panel surface
  • Author primary token recolors the sort overlay Add-sort button
  • Opening the density menu paints a non-transparent overlay surface
  • Opening the group-by menu paints a non-transparent overlay surface
  • Opening the views menu paints a non-transparent overlay surface
  • Opening the settings dialog paints a non-transparent overlay surface
  • Opening the save-view dialog paints a non-transparent overlay surface
  • Author primary token recolors the save-view dialog Save button
  • Selecting a row reveals a non-transparent bulk-actions bar
  • Author primary token tints the bulk-actions bar surface
  • The pagination footer renders a visible top border
  • Double-clicking an editable cell renders a styled inline editor (border-primary)
  • Author primary token recolors the inline editor border
  • Opening the import-CSV dialog paints a non-transparent overlay surface

Data Table Theming Guarantees

  • Zero-config table renders a non-transparent, styled surface, its header separated by a rule in a stronger border role than the frame
  • Author border + primary tokens recolor the table chrome: the frame and the header's separating rule both follow border, and a selected row repaints to a real surface

Last updated September 23, 2026

This documentation was written with AI, so errors or outdated content are possible. Sovrium is in beta. Contributions and corrections are welcome.

Built with Sovrium