Density
ERP users live in dense grids, so density is a first-class dimension, not a
theme afterthought: data-density="compact | comfortable | spacious"
on <html> or ANY wrapper remaps one token tier — rows go
30 / 40 / 48px, controls 28 / 36 / 44px, and every component follows, because
components consume only density aliases for internal sizing.
Try it (this page is live)
The header's density selector drives this whole site. Per-region override:
<html data-density="compact"> <!-- app-wide default -->
…
<div class="bo-data-table-container" tabindex="0" data-density="spacious">
<!-- one table, locally more readable — density CASCADES -->
</div>
<script>
// The complete persistence recipe:
const d = localStorage.getItem('density') ?? 'comfortable';
document.documentElement.dataset.density = d;
</script> Device/platform coverage
Audited 2026-08-15 as a follow-up to the Slice 7 Objective review — the question was whether "Web / Mobile app / RF / Tablet (Bento UI)" needs dedicated component variants. It doesn't: density + the named container-query registry already cover every one of these archetypes with the SAME components, just configured differently. No new CSS/JS came out of this — it's a verification pass, not a build.
| Archetype | Mechanism | Live example |
|---|---|---|
| Web | data-density="comfortable" (the default) + ordinary container-query response | Data table, Dashboard |
| Mobile app | data-density="compact" + the app shell's own bo-shell container query (sidebar collapses to an icon rail ≤ 56rem) | The narrow frame on Sidebar navigation, or see the container-query registry |
| Tablet (Bento UI) | bo-widget-grid's container query (span-2 widgets collapse ≤ 41rem) — a Bento-style icon/tile grid is this SAME mechanism at a different breakpoint, not a new component. The App Launch pattern uses it directly rather than inventing a separate grid. | Dashboard widget grid |
| RF / warehouse | data-density="spacious" (44px rows — the control boxes inside them do not grow; see the target-size note below) + initScanInput()'s keyboard-wedge input mode — the one archetype that genuinely needed a new input pattern, not just a layout setting | Goods receipt pattern |
Open question, not resolved here: whether RF/Mobile/Tablet are real target markets worth more investment than this documentation pass is a product decision, not something a coverage audit can settle — the recommendation is to keep documenting the mechanism regardless, since the cost is low and the value doesn't depend on the answer.
Rules that keep density safe
- Density tokens are rem-only — they scale with the user's root font, so raised text never clips. The low-vision power user runs large text + compact and gets density at a readable size; that combination is the point.
- Explicit
data-densitybeats container-query auto-compaction when set on the table or its container element itself — a distant wrapper's density tokens still inherit, but the compaction query only checks those two elements, so it stays in force. (This sentence is the canonical statement; 2026-08-21 sweep — two pages previously said "always beats", which the shipped:not([data-density])selector contradicts.) - Density moves row height and font size, not control boxes.
A checkbox, radio and the tag-input remove button are
1rem(16px) in every tier, and the data-table sort button is 18px tall. WCAG 2.5.8 is met through its spacing exception — a 24px circle centred on each of those reaches no other target, andcheck:target-sizeruns that exact spec test in CI across all three tiers — not through a 24px size floor. If you pack controls closer than 24px between centres, you break that, and the framework cannot guarantee it for markup it did not author. - Font size and row height move together per tier — never mix a density font token with a hand-set height.