Density
The data-density attribute on <html> (or any ancestor) toggles
control sizing and container paddings / gaps across the whole
subtree. Plan §9.3 specifies the two control variables;
Hypermedia Components extends the same attribute to a handful of
container vars so the layout tightens or relaxes evenly — without
that, buttons would shrink while cards and dialogs around them
stayed roomy, and the result felt unbalanced.
Control sizing (plan §9.3)
Section titled “Control sizing (plan §9.3)”| Attribute | --hc-control-height | --hc-control-padding-x |
|---|---|---|
(none) / [data-density="comfortable"] | 40px | 1rem (16px) |
[data-density="compact"] | 32px | 0.75rem (12px) |
[data-density="dense"] | 28px | 0.5rem (8px) |
hc-button and
hc-input reference these
via var() indirection on their own custom properties, so the cascade
flows: data-density → --hc-control-* → --hc-button-* /
--hc-input-* → rendered control.
Container sizing
Section titled “Container sizing”The same density attribute also overrides container paddings and inter-element gaps. These vars belong to the component layer, but the density files emit the same names at higher-specificity selectors so the override cascades automatically:
| Variable | Comfortable | Compact | Dense |
|---|---|---|---|
--hc-field-gap | 8 px | 6 px | 4 px |
--hc-toolbar-gap / padding-y | 8 px | 6 px | 4 px |
--hc-toolbar-padding-x | 12 px | 8 px | 6 px |
--hc-card-padding | 16 px | 12 px | 8 px |
--hc-dialog-padding | 16 px | 12 px | 8 px |
--hc-dialog-gap | 12 px | 8 px | 6 px |
--hc-popover-padding | 12 px | 8 px | 6 px |
--hc-alert-padding-block | 12 px | 8 px | 6 px |
--hc-alert-padding-inline | 16 px | 12 px | 8 px |
--hc-alert-gap | 8 px | 6 px | 4 px |
--hc-toast-padding-y | 12 px | 8 px | 6 px |
--hc-toast-padding-x | 16 px | 12 px | 8 px |
--hc-toast-gap | 8 px | 6 px | 4 px |
--hc-table-cell-padding-y | 8 px | 6 px | 4 px |
--hc-table-cell-padding-x | 12 px | 8 px | 6 px |
--hc-checkbox-size | 18 px | 16 px | 14 px |
--hc-radio-size | 18 px | 16 px | 14 px |
Tokens not in the table (radii, font sizes, badge / pagination paddings, dialog max-width, etc.) are deliberately not density- aware. The intent is to tighten layout, not to restyle.
Checkbox / radio sizes step from 18 → 16 → 14 px. The dense
target (14 px) is at the edge of comfortable click sizing — fine
for keyboard-driven data-entry UIs on the desktop, but pair with
larger surrounding controls if your audience includes touch users.
The sm and lg size variants shift in lockstep with the density
tier so the relative ordering sm < md < lg is preserved at every
density. The exact values:
| Component | Density | sm | md (default) | lg |
|---|---|---|---|---|
| Button | comfortable | 32 px | 40 px | 48 px |
| / Input | compact | 28 px | 32 px | 40 px |
| height | dense | 24 px | 28 px | 32 px |
| Button | comfortable | 12 px | 16 px | 20 px |
| / Input | compact | 8 px | 12 px | 16 px |
| padding-x | dense | 6 px | 8 px | 12 px |
| Checkbox | comfortable | 14 px | 18 px | 22 px |
| / Radio | compact | 12 px | 16 px | 20 px |
| size | dense | 12 px | 14 px | 18 px |
Earlier iterations kept sm and lg fixed across density tiers
under the theory that “explicit data-size should be absolute, not
relative.” That produced inversions: at dense density, the
default md button dropped to 28 px while sm stayed at 32 px,
breaking the size ordering. The current behaviour treats
sm / md / lg as relative emphasis within the active density.
Comfortable is the default. The selector
:root, [data-density="comfortable"] ensures the values apply
unconditionally so an explicit data-density="comfortable" and the
absence of the attribute behave identically.
Live preview — controls
Section titled “Live preview — controls”The three previews below render the same <button> / <input> HTML
inside containers carrying different data-density attributes.
Comfortable (default)
Section titled “Comfortable (default)”Compact
Section titled “Compact”Live preview — containers
Section titled “Live preview — containers”Cards, alerts, and toolbars also pick up the attribute. Note how the internal padding and gap tighten alongside the controls, keeping the visual rhythm proportional.
Comfortable (default)
Section titled “Comfortable (default)”Plan summary
Three seats, monthly billing.
Heads up. Two of three seats are unassigned.
Compact
Section titled “Compact”Plan summary
Three seats, monthly billing.
Heads up. Two of three seats are unassigned.
Plan summary
Three seats, monthly billing.
Heads up. Two of three seats are unassigned.
Live preview — table and form controls
Section titled “Live preview — table and form controls”Where density really earns its keep: data tables get noticeably tighter, and the checkbox / radio glyphs shrink with the surrounding controls so the form retains its visual rhythm.
Comfortable (default)
Section titled “Comfortable (default)”| Member | Role | Status |
|---|---|---|
| Akira Suzuki | Editor | Active |
| Mika Tanaka | Reader | Pending |
| Yuki Sato | Owner | Active |
Notify on changes
Free
Pro
Compact
Section titled “Compact”| Member | Role | Status |
|---|---|---|
| Akira Suzuki | Editor | Active |
| Mika Tanaka | Reader | Pending |
| Yuki Sato | Owner | Active |
Notify on changes
Free
Pro
| Member | Role | Status |
|---|---|---|
| Akira Suzuki | Editor | Active |
| Mika Tanaka | Reader | Pending |
| Yuki Sato | Owner | Active |
Notify on changes
Free
Pro
Applying it to your page
Section titled “Applying it to your page”Set the attribute as high in the DOM as is useful. Most apps want it
on <html> so the choice cascades to every control:
<html data-density="compact"> ...</html>For a single section of denser UI (e.g. a data-table toolbar) attach it locally:
<section data-density="dense"> <div class="hc-toolbar"> <button class="hc-button">Edit</button> <button class="hc-button">Delete</button> </div></section>Why this works
Section titled “Why this works”For the control tier (--hc-button-*, --hc-input-*), the token
build emits component-level custom properties as var() references:
--hc-button-height: var(--hc-control-height);--hc-button-padding-x: var(--hc-control-padding-x);When data-density="compact" overrides --hc-control-height to
32px, every consumer of --hc-button-height resolves at the new
value without explicit rules per component.
For the container tier (--hc-card-padding, --hc-dialog-gap,
etc.), the density files emit the same variable names at higher-
specificity selectors ([data-density="compact"],
[data-density="dense"]). The cascade picks the matching block over
the component file’s :root rule, so card / dialog / popover / alert
/ toast / toolbar CSS reads the active density value automatically.
Per-component customization still works at both tiers — setting
--hc-button-height: 56px; or --hc-card-padding: 0; on a scope
overrides the density value because the per-element declaration is
more specific.
Size variants are independent
Section titled “Size variants are independent”data-size="sm" and data-size="lg" on buttons / inputs set their
own dedicated variables (--hc-button-sm-height, etc.) and are not
affected by data-density. They mean different things — “this one
control is a bigger CTA” vs. “the whole UI is laid out tightly.”
CSS variables
Section titled “CSS variables”--hc-control-height --hc-card-padding--hc-control-padding-x --hc-dialog-padding--hc-field-gap --hc-dialog-gap--hc-toolbar-gap --hc-popover-padding--hc-toolbar-padding-y --hc-alert-padding-block--hc-toolbar-padding-x --hc-alert-padding-inline --hc-alert-gap --hc-toast-padding-y --hc-toast-padding-x --hc-toast-gapAll emitted under :root, [data-density="comfortable"],
[data-density="compact"], and [data-density="dense"].