Skip to content

Utilities

Three families of utility class, from least to most opt-in: the always-on .sr-only, component conventions the theme already ships, and a generated sheet of token-derived helpers you import separately. It also covers the JavaScript helpers the package exports.

Always available: .sr-only

You already have this: there is nothing to opt into beyond the theme itself. .sr-only hides content visually while keeping it available to screen readers. The content stays in the accessibility tree, clipped to a 1px box and taken out of the visual flow. It ships in the reference theme's base.css, unlayered, whenever @hyzer-labs/ui/theme is imported. Button (loading label), Link (external-link hint), Checkbox, RadioGroup, Toggle, and the Field scaffold's hideLabel already render class="sr-only".

<button type="button">
	<IconX aria-hidden="true" />
	<span class="sr-only">Close</span>
</button>

No component ships the rule itself; components only ever emit the class name. The theme is what makes it visually hidden. Source, from theme/base.css:

.sr-only {
	position: absolute;
	width: 1px;
	height: 1px;
	padding: 0;
	margin: -1px;
	overflow: hidden;
	clip: rect(0, 0, 0, 0);
	white-space: nowrap;
	border-width: 0;
}

Opt-in component classes

One step up: apply a class the theme already ships, no new import required. Conventions like .hz-card-title and .hz-banner-title ship inside their own component theme sheets (theme/components/card.css, theme/components/banner.css) rather than the generated utility sheet. They style whatever heading or lead element you bring, at whatever level the page needs, because headings are not load-bearing for Card or Banner the way they are for Modal or Accordion. The full catalog, alongside every component's data-*/hz-* hooks, lives on Theming → Styling Components.

Course conditions

Fairways dry, greens rolling fast.
<Card class="hz-card--outlined">
	<h3 class="hz-card-title">Course conditions</h3>
	Fairways dry, greens rolling fast.
</Card>
Tee times open Monday Book early. Weekends fill fast.
<Banner intent="info">
	<strong class="hz-banner-title">Tee times open Monday</strong>
	Book early. Weekends fill fast.
</Banner>

See Card and Banner for the full component reference.

The opt-in sheet

The biggest commitment of the three: an additional sheet, imported explicitly. A utility class is a token-derived, single-property helper: one class, one declaration, resolved from a design token. That is the whole definition; it does not depend on where the class is or is not used.

Utilities are for ad-hoc spots like nudging one element or tinting one line of text. They are not an alternative layout system. Components already own their spacing (gap/padding props, the data-padding/data-gap scales) and the density system (--hz-space-near/--hz-space-away). The utility sheet deliberately does not reproduce that surface: it exposes the fixed --hz-space-* scale for margins only, never the density near/away distances, and no padding helpers at all (padding is owned by components).

utilities.css is generated output, exactly like tokens.css: rendered from the same resolved token model by hyzer generate --utilities, never edited by hand. It is opt-in. Import it explicitly, like a theme sheet, and if you never import it you ship none of these bytes. If your config extends the intent vocabulary or the space scale, the matching classes are generated automatically, with nothing to hand-maintain.

$ hyzer generate --utilities
No config found (looked for hyzer.config.ts, hyzer.config.js, hyzer.config.mjs), using the base schema.
wrote hyzer-tokens.css (full, 84 tokens)
wrote hyzer-utilities.css
contrast: 92 pairings checked, all pass WCAG AA

The flag opts in for a single run; set utilities: true (or { output: '...' }) in hyzer.config.ts to opt in every run. The full config surface is on Config & CLI.

import '@hyzer-labs/ui/utilities.css';

Every rule is unlayered, single-class specificity (0,1,0), with no !important: the same posture as .sr-only above. A deliberately applied utility beats the layered reference theme. Your own unlayered class of equal specificity still wins on source order, so import your stylesheet after the utility sheet if you mean to override one.

Color utilities

Four families (.hz-text-*, .hz-bg-*, .hz-border-* and .hz-fill-*), each with its role helpers plus one class per resolved intent. One property each, so .hz-border-danger sets border-color and nothing else: bring your own width and style. .hz-fill-* is for SVG.

These are for ad-hoc spots. Components that own a tinted surface (Badge, Alert, Banner) keep doing that themselves, and reaching for a background utility to restyle one is the wrong tool. Use their intent prop or their theme hooks.

Free shipping over $75. In stock

<p>
	Free shipping over $75.
	<span class="hz-text-success hz-m-inline-start-xs">In stock</span>
</p>
ClassDeclarationNote
.hz-textcolor: var(--hz-color-text)Resets inherited color back to the base text role — e.g. inside a tinted region.
.hz-text-mutedcolor: var(--hz-color-text-muted)The muted text role — de-emphasized copy.
.hz-bgbackground-color: var(--hz-color-surface)The base surface role.
.hz-bg-mutedbackground-color: var(--hz-color-surface-muted)The subdued opaque surface — quiet panels, code blocks.
.hz-borderborder-color: var(--hz-color-border)Color only. Bring your own border-width and border-style.
.hz-fillfill: var(--hz-color-text)For SVG. Matches body text.
.hz-fill-mutedfill: var(--hz-color-text-muted)For SVG.

And one class per intent, in every family. Every cell references the intent token, so a remapped or added intent flows straight through with no regeneration needed:

Intentcolorbackground-colorborder-colorfillPreview
--hz-intent-neutral.hz-text-neutral.hz-bg-neutral.hz-border-neutral.hz-fill-neutralneutral
--hz-intent-primary.hz-text-primary.hz-bg-primary.hz-border-primary.hz-fill-primaryprimary
--hz-intent-secondary.hz-text-secondary.hz-bg-secondary.hz-border-secondary.hz-fill-secondarysecondary
--hz-intent-danger.hz-text-danger.hz-bg-danger.hz-border-danger.hz-fill-dangerdanger
--hz-intent-warning.hz-text-warning.hz-bg-warning.hz-border-warning.hz-fill-warningwarning
--hz-intent-success.hz-text-success.hz-bg-success.hz-border-success.hz-fill-successsuccess
--hz-intent-info.hz-text-info.hz-bg-info.hz-border-info.hz-fill-infoinfo

Margin utilities

Logical properties only, with no mt/mb/ml/mr, so a single-direction nudge (block-start, block-end, inline-start, inline-end) stays correct under RTL and vertical writing modes. Seven families per --hz-space-* rung. Every cell below references the token var, so your space override flows straight through with no regeneration needed.

FamilyPropertynonexssmmdlgxl
All sidesmargin.hz-m-none.hz-m-xs.hz-m-sm.hz-m-md.hz-m-lg.hz-m-xl
Block axis (both)margin-block.hz-m-block-none.hz-m-block-xs.hz-m-block-sm.hz-m-block-md.hz-m-block-lg.hz-m-block-xl
Block startmargin-block-start.hz-m-block-start-none.hz-m-block-start-xs.hz-m-block-start-sm.hz-m-block-start-md.hz-m-block-start-lg.hz-m-block-start-xl
Block endmargin-block-end.hz-m-block-end-none.hz-m-block-end-xs.hz-m-block-end-sm.hz-m-block-end-md.hz-m-block-end-lg.hz-m-block-end-xl
Inline axis (both)margin-inline.hz-m-inline-none.hz-m-inline-xs.hz-m-inline-sm.hz-m-inline-md.hz-m-inline-lg.hz-m-inline-xl
Inline startmargin-inline-start.hz-m-inline-start-none.hz-m-inline-start-xs.hz-m-inline-start-sm.hz-m-inline-start-md.hz-m-inline-start-lg.hz-m-inline-start-xl
Inline endmargin-inline-end.hz-m-inline-end-none.hz-m-inline-end-xs.hz-m-inline-end-sm.hz-m-inline-end-md.hz-m-inline-end-lg.hz-m-inline-end-xl

A visual scale. Each chip's leading gap is a real margin-inline-start utility, walking the space scale from none to xl:

nonexssmmdlgxl

Source

The generated sheet, in full:

Contrast: what is graded and what is not

Every .hz-text-<intent> class maps to a pairing already checked against WCAG AA: intent text colors are AA-verified on the two surface roles only (--hz-color-surface and --hz-color-surface-muted, both light and dark). On any other background, contrast is yours to check.

That includes a background utility. .hz-bg-danger is not one of those two surfaces, so .hz-text-danger on .hz-bg-danger is a pairing nothing has graded, and several of the intents fail it outright. The combination the report does cover is the one Banner ships: text in the surface role on a solid intent fill, which flips with the mode and stays legible in both. Reach for .hz-bg or .hz-bg-muted under intent-colored text. For a solid intent fill, set the text to the surface role rather than an intent.

.hz-border-* is exempt either way: a border is non-text, so it answers to WCAG 1.4.11 (3:1 against what it sits on) rather than to the 4.5:1 text rule.

Beyond that the sheet introduces no new pairings. See Contrast & Accessibility for the full report, and Colors & Intent for the intent vocabulary itself.

JavaScript utilities

Not every utility is a class. The package also exports plain functions that are pure and SSR-safe, and touch no DOM:

  • Contrast math: contrastRatio, gradeContrast, bestLevel, bestLevelLarge, relativeLuminance, mixSrgb, hexToRgb, and rgbToHex, from the package root or @hyzer-labs/ui/utils. Grade any two hex colors against WCAG with the same functions behind the pairing checker. See Verify your palette for using them in a unit test.
  • toFormErrors: reshapes a plain field-name → message record into the error array Form renders as its linked summary.
  • announce: sends a message to a reused, visually hidden live region for screen readers, from @hyzer-labs/ui/observers. It pairs naturally with an observer callback that just loaded more content or noticed a change.