Skip to content

Spacing & Sizing

Three sizing systems, each answering a different question: a fixed spacing scale for explicit distances, density spacing that tightens as content nests, and breakpoint width tokens.

  • Spacing scale: fixed steps (--hz-space-xs … xl) for explicit, context-independent distances; they back the component defaults.
  • Density spacing: two context-aware distances (near / away), derived from one grid unit, that tighten automatically as content nests. Use these for page rhythm instead of picking steps by hand.
  • Breakpoint widths: the --hz-width-sm…xl tokens that cap Container and drive the Grid/Split container-query thresholds.

Spacing tokens

--hz-space-none 0
--hz-space-xs 0.5rem
--hz-space-sm 1rem
--hz-space-md 2rem
--hz-space-lg 4rem
--hz-space-xl 8rem

Density spacing

An alternate spacing model, adapted from Complementary Space. Instead of picking from a scale, use two distances: --hz-space-near between related things and --hz-space-away between unrelated things. Both derive from the --hz-density grid unit (0.4rem), so overriding one custom property retunes every distance on the page.

The body starts un-shifted — depth 0 in the table below. Each data-density-shift on an ancestor moves everything inside it down one row, so nested regions read denser without introducing new spacing values. Depth 3 is the floor: a fourth nested shift keeps depth 3's values. The near multipliers walk the 1-2-5-10 ladder, so a shifted region's away always equals its parent's near.
Shift depthRung (custom property)--hz-space-near--hz-space-away
none (body)--hz-density-ladder-depth-110 × = 4rem20 × = 8rem
1 × data-density-shift--hz-density-ladder-depth-25 × = 2rem10 × = 4rem
2 × data-density-shift--hz-density-ladder-depth-32 × = 0.8rem5 × = 2rem
3 × data-density-shift--hz-density-ladder-depth-41 × = 0.4rem2 × = 0.8rem

Live demo

A real composition built from Stack and Cluster: page, sections, cards, tag rows. The code is what you'd write on a fresh top-level page. Every distance is gap="near", gap="away", or padding="near", and each nested region adds one data-density-shift so the whole hierarchy tightens on its own.

Projects

Flight Tracker

Round scoring and disc flight stats for every throw.

sveltesupabasepwa

Course Atlas

Community-maintained maps of local courses.

sveltekitmaplibre

Archive

Retired experiments live here.

<Stack gap="away">                           <!-- sections: away = 8rem -->
	<Stack gap="near" data-density-shift>       <!-- section: 1 shift → near = 2rem -->
		<h2>Projects</h2>
		<Cluster gap="near" align="stretch">
			<Stack padding="near">                   <!-- card inherits the shift: 2rem -->
				<Stack gap="near" data-density-shift>   <!-- card rhythm: 2 shifts → 0.8rem -->
					<h3>Flight Tracker</h3>
					<p>Round scoring and disc flight stats.</p>
					<Cluster gap="near" data-density-shift> <!-- tags: 3 shifts → 0.4rem -->
						<span>svelte</span>
						<span>supabase</span>
						<span>pwa</span>
					</Cluster>
				</Stack>
			</Stack>
			<!-- … more cards … -->
		</Cluster>
	</Stack>

	<Stack gap="near" data-density-shift>
		<h2>Archive</h2>
		<p>Retired experiments live here.</p>
	</Stack>
</Stack>

The preview compensates for where it sits. This section is already two density levels deep: the docs shell and this page section each add a shift. Each ambient level costs one rung on the ladder, so the live version swaps each near for away (one rung back up) and drops the shifts the shell already provides. Every distance then renders at its fresh-page value: heading and cards at 2rem, card rhythm at 0.8rem, tag gaps at 0.4rem. The one exception is the 8rem section spacing, which the tightest level caps at 2rem here. Three levels is as tight as the scale goes: a fourth nested shift keeps the third level's values.

Bring your own scale

Two ways to retune the density system, depending on how far you want to go.

1. Proportional retune. Set --hz-density (or tokens.density.unit in the config). Same rhythm, a different unit: every distance above scales together.

2. Alias the rungs to your own scale. A rung is the custom property behind one shift depth, named in the Rung column above. --hz-density-ladder-depth-1 holds the unshifted body's distance, -depth-2 the first data-density-shift, and so on to -depth-4. Write one block, at :root or on any subtree, with one line per depth you want to change:

:root {
	--hz-density-ladder-depth-1: var(--space-10); /* body level */
	--hz-density-ladder-depth-2: var(--space-6); /* 1 × data-density-shift */
	--hz-density-ladder-depth-3: var(--space-3); /* 2 × */
	--hz-density-ladder-depth-4: var(--space-2); /* 3 × */
}

Every rung you leave alone stays on the built-in ladder. Each rung does two jobs: depth 2's rung is depth 2's near and depth 3's away, which is what keeps the ladder consistent. The body level's away is its own rung doubled, so the top of the scale follows depth 1 automatically.

Width / breakpoint tokens

Overriding these tokens retunes Container max-widths, Split's stackBelow threshold, and Grid's fluid { min } mode, because they all resolve via var(). The exceptions are Grid's band breakpoints (base/sm/md/lg) and Header/Toc's named collapse tiers: all of them mirror these values but stay literal system constants, because CSS cannot read custom properties inside media or container queries. On Header/Toc specifically, pass a px number instead of a named tier: a number is resolved at runtime, so it follows your retuned scale.

TokenValue
--hz-width-sm640px
--hz-width-md968px
--hz-width-lg1200px
--hz-width-xl1440px
--hz-width-full100%

Override the spacing scale, the density unit, or a width token with a plain CSS custom property, or set space, density, and width in the hyzer config; see Theming → Tokens & Overrides.

Logical axes

Where a component takes spacing per axis (the layout primitives' paddingInline and paddingBlock props), the names are the CSS logical properties they set, not physical x/y. The inline axis runs along the line of text; the block axis runs across it. That keeps the props correct in every writing mode: in RTL layouts the inline axis flips with the text, and in vertical writing modes it runs top-to-bottom, where a physical “x” would pad the wrong edges.

The library's own CSS follows the same rule (centering is margin-inline: auto, gutters are padding-inline), so each prop maps 1:1 onto the property it drives. padding remains the both-axes shorthand; the per-axis longhands win where set.

Positioning follows the same logical-first rule for placing a tooltip, popover, or dropdown menu next to its trigger.