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.
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.
: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 atable, 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.
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: roomyIt 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
primarytoken recolors the filter overlay Add-filter button - Opening the sort overlay paints a non-transparent panel surface
- Author
primarytoken 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
primarytoken recolors the save-view dialog Save button - Selecting a row reveals a non-transparent bulk-actions bar
- Author
primarytoken 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
primarytoken recolors the inline editor border - Opening the import-CSV dialog paints a non-transparent overlay surface
Data Table Theming Guarantees
- Zero-config
tablerenders a non-transparent, styled surface, its header separated by a rule in a stronger border role than the frame - Author
border+primarytokens recolor the table chrome: the frame and the header's separating rule both followborder, 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.