Skip to content

Example Themes

Three example themes, sorted by how much of the reference theme each one keeps. Ocean keeps all of it and only redefines --hz-* tokens. Docs keeps all of it too and layers one class-hook override on top, with no palette of its own. Terminal keeps none of it. Ocean and Terminal are generated from a hyzer.config.ts beside them. Like the base tokens, they meet WCAG AA on every graded pairing, in the default theme and in dark. All three, compared:

OceanDocsTerminal
Palette via configyesnone — rides the reference themeyes
Class-hook overridesnoneone (.docs-table)yes
Reference themerequiredlayers over itnot imported
Adds new intentsnonophosphor, amber
Scoped to a classruntime-scoped demo onlyunscoped — no class to add anywhere.hz-theme-terminal

Ocean is one end: it shows how far tokens alone get you. Terminal is the other end: it never imports the reference theme, so every rule in it is its own. It also grows the system rather than only recoloring it, with two intents the library has never heard of, type-checked and contrast-graded like any built-in. Docs sits between them: the same class-hook contract every component ships is enough to extend the reference theme without forking it. If you only read one file, read Terminal's button.css: that is what "headless" actually buys you.

Ocean: tokens only

Not one class hook. Every difference below comes from redefining --hz-* tokens; the reference theme does the rest of the work, unchanged.

import '@hyzer-labs/ui/tokens.css';
import '@hyzer-labs/ui/theme';
import '@hyzer-labs/ui/theme/examples/ocean.css';
Par saved Windy Hole 7 OB Casual

Course update

Hole 7 tee pad moves back to the long position this weekend.

Next tee time

Saturday, 8:40, Maple Hill.

What is in the bag?

Three drivers, a midrange, and misplaced optimism.

Course conditions

Fairways dry, greens rolling fast.

Docs: layered on the reference theme

Adds no palette at all. Every rule below resolves through the reference theme’s own role and intent tokens, unchanged. On top it layers page-rhythm scaffold classes and one class-hook override. You import it like any other theme sheet, and it needs no class anywhere.

The full .docs-table walkthrough lives on Styling Components: why unlayered beats layered, with a demo and the source. This is, verbatim, the sheet design.hyzer.sh imports for its own chrome. You're reading it right now.

import '@hyzer-labs/ui/tokens.css';
import '@hyzer-labs/ui/theme';                     // layers under it
import '@hyzer-labs/ui/theme/examples/docs/docs.css';
Par saved Windy Hole 7 OB Casual

Course update

Hole 7 tee pad moves back to the long position this weekend.

Next tee time

Saturday, 8:40, Maple Hill.

What is in the bag?

Three drivers, a midrange, and misplaced optimism.

Course conditions

Fairways dry, greens rolling fast.

Terminal: standalone

Every rule below is the example’s own, written against the raw headless hooks. The components ship structure, behavior and ARIA. Nothing here inherits a visual decision from the reference theme.

import '@hyzer-labs/ui/tokens.css';
// NO '@hyzer-labs/ui/theme': that is the point.
import '@hyzer-labs/ui/theme/examples/terminal/terminal.css';
Par saved Windy Hole 7 OB Casual

Course update

Hole 7 tee pad moves back to the long position this weekend.

Next tee time

Saturday, 8:40, Maple Hill.

What is in the bag?

Three drivers, a midrange, and misplaced optimism.

Course conditions

Fairways dry, greens rolling fast.

Style one instance by hand

Every demo above uses the same markup for the "Go pro" button: <Button class="cta">. The class prop is merged after the component's hz-button root class, so it lands on the element ready for you to style. Sheet-level and instance-level overrides compose. Ocean and Terminal each add one, below. Docs adds none: an unscoped sheet has no class to hang a demo-only rule on, so its "Go pro" button renders exactly as the reference theme paints it.

<Button class="cta">Book a round</Button>

<style>
	/* Rendered as class="hz-button cta". One instance, styled by you.
	   The theme sheet never mentions .cta at all. */
	:global(.hz-theme-terminal .hz-button.cta) {
		box-shadow: 8px 8px 0 var(--hz-intent-danger);
		translate: -2px -2px;
	}
</style>

Note the selector: .hz-theme-terminal .hz-button.cta, not a bare .cta. Terminal is the one example here that roots its overrides at a class. The section below covers why.

Grow the intent vocabulary

The intents the library ships are a starting set. A component only stamps data-intent="<name>" and lets the theme decide what that name looks like. So the set of intents belongs to your theme. Terminal adds two: phosphor and amber, the two tubes every real terminal shipped with. They are the last two buttons in its demo above.

Three steps. With the reference theme, the last one is nothing at all:

  1. Define the token in your config. The generator emits it as --hz-intent-amber, and the contrast report grades it exactly like primary. A custom intent meets the same AA bar as a built-in one.
  2. Register the type by augmenting IntentRegistry. Now intent="amber" type-checks and autocompletes on every component, and intent="ambr" is a compile error. See the intents.d.ts tab above.
  3. Nothing to style. With the reference theme, running hyzer generate wires the intent into every component that takes one (Button, Badge, Alert, and the rest) the moment the token exists.
// hyzer.config.ts: define the token, and it gets contrast-graded
import { defineConfig } from '@hyzer-labs/ui/config';

export default defineConfig({
	tokens: {
		intent: {
			phosphor: 'var(--hz-palette-primary)',
			amber: '#ffb000'
		}
	}
});

Augment the module that declares the interface (@hyzer-labs/ui/types), not the barrel that re-exports it. TypeScript merges an interface only into its declaring module, so declare module '@hyzer-labs/ui' would silently do nothing.

A theme that replaces the reference theme outright, like Terminal, owns its intent vocabulary and maps it by hand. That means one rule per intent, per component, which is what a built-in intent needs there too:

/* a standalone theme sheet: one rule per intent, same shape as the built-ins */
.hz-button[data-intent='amber'] {
	--hz-button-accent: var(--hz-intent-amber);
}

How these work

Why does Terminal use a class instead of :root?

Every rule in Terminal is rooted at a class: .hz-theme-terminal .hz-button rather than .hz-button. The engine roots its token block at that class instead of :root, through the selector config key (set in Terminal's own hyzer.config tab above) or the --selector flag. Scope the sheet to a class covers both. So the theme travels with the class: put it on <html> to own a document, or on one element to own a panel. That is why this page can render an Ocean panel and a Terminal panel side by side without them fighting. It is also why importing this sheet does not disturb the docs site's own theme.

Start from one

Copy a config into your project as hyzer.config.ts, run hyzer generate, and iterate. The contrast report grades every change. To go further than color, copy the theme's components/ folder too and edit the hooks directly. Styling Components is the reference for what each hook exposes, and Tokens & Overrides covers the token workflow.

Coverage: Terminal restyles Button, Badge, Alert, Card, the Field scaffold, TextInput, Toggle, Tabs, and Accordion. It imports no theme, so anything else falls back to bare headless structure. Docs takes the opposite approach: it restyles no bare component hook, so a Table with no .docs-table wrapper looks exactly as the reference theme paints it. Over that theme it adds only scaffold classes, content chrome, and that one opt-in wrapper.