Skip to content

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.

Attribute--hc-control-height--hc-control-padding-x
(none) / [data-density="comfortable"]40px1rem (16px)
[data-density="compact"]32px0.75rem (12px)
[data-density="dense"]28px0.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.

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:

VariableComfortableCompactDense
--hc-field-gap8 px6 px4 px
--hc-toolbar-gap / padding-y8 px6 px4 px
--hc-toolbar-padding-x12 px8 px6 px
--hc-card-padding16 px12 px8 px
--hc-dialog-padding16 px12 px8 px
--hc-dialog-gap12 px8 px6 px
--hc-popover-padding12 px8 px6 px
--hc-alert-padding-block12 px8 px6 px
--hc-alert-padding-inline16 px12 px8 px
--hc-alert-gap8 px6 px4 px
--hc-toast-padding-y12 px8 px6 px
--hc-toast-padding-x16 px12 px8 px
--hc-toast-gap8 px6 px4 px
--hc-table-cell-padding-y8 px6 px4 px
--hc-table-cell-padding-x12 px8 px6 px
--hc-checkbox-size18 px16 px14 px
--hc-radio-size18 px16 px14 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:

ComponentDensitysmmd (default)lg
Buttoncomfortable32 px40 px48 px
/ Inputcompact28 px32 px40 px
heightdense24 px28 px32 px
Buttoncomfortable12 px16 px20 px
/ Inputcompact8 px12 px16 px
padding-xdense6 px8 px12 px
Checkboxcomfortable14 px18 px22 px
/ Radiocompact12 px16 px20 px
sizedense12 px14 px18 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.

The three previews below render the same <button> / <input> HTML inside containers carrying different data-density attributes.

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.

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.

Plan summary

Three seats, monthly billing.

Heads up. Two of three seats are unassigned.

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.

MemberRoleStatus
Akira SuzukiEditorActive
Mika TanakaReaderPending
Yuki SatoOwnerActive
MemberRoleStatus
Akira SuzukiEditorActive
Mika TanakaReaderPending
Yuki SatoOwnerActive
MemberRoleStatus
Akira SuzukiEditorActive
Mika TanakaReaderPending
Yuki SatoOwnerActive

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>

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.

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.”

--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-gap

All emitted under :root, [data-density="comfortable"], [data-density="compact"], and [data-density="dense"].

  • Tokens overview — the four-layer model.
  • Button — density-aware.
  • Input — density-aware.
  • Card — density-aware padding.
  • Dialog — density-aware padding + gap.
  • Alert — density-aware padding + gap.
  • Toolbar — density-aware gap + padding.