Skip to content

Theming

The components are headless: they ship structure, behavior, and accessibility, and every visual decision stays yours to keep, override, or replace. Styling arrives in opt-in tiers, each importable on its own.

The tiers

TierImportWhat it gives you
Headless components@hyzer-labs/uiStructure, behavior, ARIA, and keyboard support. Stable hz-* classes and data-* hooks, plus native element defaults. No appearance opinions.
Resetreset.cssA structural-only reset in @layer hz-reset. No colors, no fonts.
Tokenstokens.cssThe --hz-* custom properties: palette, roles, intents, type, spacing, radius, motion. They are generated from one schema. See Tokens & Overrides.
Reference themethemeA full, token-driven look for every component, in @layer hz-theme. It is the styled starting point this site runs on. You can take single components from it via theme/components/*.css.
Utilitiesutilities.cssSingle-property helper classes built from tokens: text-color roles and intents, plus logical margins. Use them for one-off spots. They are opt-in, imported like the theme, and cost you nothing if you skip them. See Utilities.
Your overridesyour CSS / hyzer.config.tsToken overrides in plain CSS, generated sheets from the hyzer CLI, or component-level styling on the hz-* hooks. See Example Themes.
/* Each line is optional. Every tier works without the ones below it. */
@import '@hyzer-labs/ui/reset.css';     /* structural reset (@layer hz-reset) */
@import '@hyzer-labs/ui/tokens.css';    /* the --hz-* custom properties */
@import '@hyzer-labs/ui/theme';         /* the reference theme (@layer hz-theme) */
@import '@hyzer-labs/ui/utilities.css'; /* opt-in utility classes (unlayered) */
@import './your-overrides.css';         /* unlayered: always wins */

How the cascade layers work

Every reference-theme rule lives in the hz-theme cascade layer. Each rule is wrapped in :where(), so it stays at single-class specificity. Your stylesheet is unlayered, and unlayered CSS always beats layered CSS. You will never need !important, a copy of the theme's selector, or specificity math to override it.

/* Pinned by both reset.css and theme.css, so import order never matters: */
@layer hz-reset, hz-theme;

/* Your stylesheet is UNLAYERED. Unlayered CSS beats every @layer rule.
   So any selector you write overrides the theme with no specificity
   fight, even a bare class: */
.my-quiet-button {
	background: transparent;
}

Every component also takes a class prop, merged onto the same element as its hz-* root class. Class order in the attribute has no effect on the cascade. Your rule wins because your stylesheet is unlayered and the theme is not.

What the theme paints before you write a class

Importing the reference theme styles a handful of bare elements, with no class of yours involved. That is the whole list:

  • body takes the surface and text roles, the sans font stack, and the base line height.
  • :focus-visible gets a two-pixel primary outline at a two-pixel offset.
  • A bare <a> takes primary, and :visited takes secondary. Browser blue and purple fail on a dark surface. That is why this rule lives in the theme rather than the reset.
  • <kbd> renders as a key: mono font, muted text on the muted surface, a thin border, and a small radius. Press Escape to see one.
  • .sr-only hides content visually while leaving it for screen readers. Components emit this class, so the theme has to ship the rule.

Each of these except .sr-only is wrapped in :where(), so it sits at zero specificity. Any class of yours wins, and any element you style yourself is untouched. .sr-only is unlayered on purpose, so a consumer layer that reorders the cascade cannot accidentally reveal hidden text.

Where to override what

GoalMechanism
The whole app to look differentOverride --hz-* tokens: the tokens key in a config, or plain CSS. Nothing goes on an element; the page is the scope. See Tokens & Overrides.
A region, or the whole page, to carry a look you named at build timeDefine it under themes, then set data-theme="<name>" on any element. theme('<name>') writes that same attribute, so use whichever suits the markup. Dark is one such name. See Section themes.
A look that comes from data: a per-tenant accent, or a color a reader pickedtheme(object). It resolves in the browser and writes inline custom properties, so no build-time entry is needed. It is not contrast-graded. See Section themes.
A region that needs its own palette and must still follow the page between light and darkGenerate a second sheet scoped to a class and put the class on the region. data-theme is then still free to carry light or dark. A themes entry cannot do both: one attribute holds one value. See Scope the sheet to a class.
Restyle one component, keep the restWrite unlayered CSS against its hz-* class and data-* hooks, or use the class prop. See Styling Components.
A different look entirelySkip the reference theme and style the headless hooks from scratch. The tokens still help, but nothing requires them.
Verify a palette still meets WCAGUse the exported contrast utilities and the CLI's report. See Contrast & Accessibility.

Where a rule lives decides whether it needs :global(). A stylesheet you import never does, because Svelte only scopes styles inside a component's <style> block. A rule in your own component's <style> block does need it for a class you passed to a library component. That class lands on markup the component renders, so an unwrapped selector is pruned as unused. Custom-property hooks need no selector at all: set them on any ancestor and they inherit through the boundary. Styling Components walks through all three.