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:
| Property | Density | sm | md (default) | lg |
|---|---|---|---|---|
| Button / input height | comfortable | 32 px | 40 px | 48 px |
| compact | 28 px | 32 px | 40 px | |
| dense | 24 px | 28 px | 32 px | |
| Button / input padding-x | comfortable | 12 px | 16 px | 20 px |
| compact | 8 px | 12 px | 16 px | |
| dense | 6 px | 8 px | 12 px | |
| Checkbox / radio size | comfortable | 14 px | 18 px | 22 px |
| compact | 12 px | 16 px | 20 px | |
| 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 |
Compact
Section titled “Compact”| Member | Role | Status |
|---|---|---|
| Akira Suzuki | Editor | Active |
| Mika Tanaka | Reader | Pending |
| Yuki Sato | Owner | Active |
| Member | Role | Status |
|---|---|---|
| Akira Suzuki | Editor | Active |
| Mika Tanaka | Reader | Pending |
| Yuki Sato | Owner | Active |
Touch targets
Section titled “Touch targets”Density trades target size for information density — pick the tier by pointer, not by taste:
comfortableis the touch tier. Controls are 40 px tall (48 px atdata-size="lg"), clearing the WCAG 2.5.8 target-size minimum (24 × 24 px) with room to spare; putdata-size="lg"on primary touch actions to reach the 44–48 px that platform HIGs (Apple / Material) recommend. Checkbox and radio glyphs are 18 px, but the wrapping.hc-checkbox-label/.hc-radio-labelextends the clickable area well beyond the glyph.compactanddenseare pointer-fine (mouse / trackpad) presets for information-dense desktop surfaces — datagrids, consoles, admin toolbars. Atdense, buttons drop to 28 px and checkbox / radio glyphs to 14 px, below every touch guideline.
Don’t ship compact or dense as the only density on a touch-first
screen, and if your app offers a density picker, gate it on the
pointer:
<script type="module"> // Coarse pointers (touch) always get comfortable targets. if (matchMedia('(pointer: coarse)').matches) { document.documentElement.setAttribute('data-density', 'comfortable'); }</script>Flipping the attribute is the recommended reset because each tier
re-emits the same custom-property names under its own attribute
selector (see Why this works) — there are no
per-tier --hc-*-comfortable variables a media query could reference.
A CSS-only alternative therefore means duplicating values: copy the
comfortable column of the tables above into an un-layered
@media (pointer: coarse) { [data-density] { … } } block (un-layered
author CSS beats the kit’s @layer hc.tokens, so no specificity
fight). That copy goes stale whenever the tokens change, which is why
the one-line attribute reset above is preferred.
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 and density compose
Section titled “Size variants and density compose”data-size picks one control’s rung on the size ladder (“this one
control is a bigger CTA”); data-density moves the whole ladder
(“the entire UI is laid out tightly”). The sm / lg variables shift
with the density tier — see the table above — so the relative ordering
sm < md < lg is preserved at every density.
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-gap--hc-table-cell-padding-y --hc-checkbox-size--hc-table-cell-padding-x --hc-radio-sizeAll emitted under :root, [data-density="comfortable"],
[data-density="compact"], and [data-density="dense"].