Skip to content

Section Themes

A theme does not have to own the whole page. Any element can carry one and everything inside it follows, so a single long page can change character section by section.

Name a theme

theme('dark') puts data-theme="dark" on the element. Each band below carries a different theme, on the same page, at the same time.

default

The default theme, restored explicitly, even inside a dark page.

dark

Surfaces, text, borders and every intent follow the attribute.

inline

An override object, resolved at runtime, with no config entry needed. The square corners and bolder label come from that object too: a theme can carry type and radii, not only color.

import { theme } from '@hyzer-labs/ui';

<section {@attach theme('dark')}>
	<!-- everything in here is dark, whatever the page around it is -->
</section>

It really is just an attribute

The attachment is a convenience. It writes one attribute and restores whatever was there when it unmounts. Writing the attribute yourself is just as valid, and it works without JavaScript.
<!-- identical result, no import: this is all the attachment writes -->
<section data-theme="dark">…</section>

Weighing this against a class scope or a plain token override? See Where to override what on Theming Overview.

Define your themes

Named themes come from the themes map in your config. Each entry becomes one [data-theme="…"] block in the generated sheet, and a themed section with color of its own is graded for contrast the same way the built-in dark theme is — a theme with no color has nothing to grade.

A theme entry takes any group tokens takes: type, spacing, radii, motion, not only color. The inline override object in the section above is the same shape. A theme is a token override, named or inline.

// hyzer.config.ts
import { defineConfig } from '@hyzer-labs/ui/config';

export default defineConfig({
	themes: {
		dark: { palette: { primary: '#60a5fa' } },  // the built-in, yours to extend
		ocean: { palette: { primary: '#0ea5e9' } }, // data-theme="ocean"
		'ocean-dark': { color: { surface: '#0b1120' } }
	}
});

dark comes free. You need no themes.dark entry to have it: the library authors a complete dark theme, seeded from its contrast-tuned hues, and the prefers-color-scheme block above follows it. Add a dark entry only when you want to change dark. What you set there merges over what the library already authors rather than replacing it.

You cannot rename dark, because the name is not this library's. It is the platform's: it matches prefers-color-scheme: dark and color-scheme: dark, and the reference theme's own rules use the same word.

Your default block has no platform name, so you choose one. defaultThemeName sets it, and it is 'default' until you change it. That name is reserved as a themes key: the default theme is the :root block you author through tokens, not a themes entry. [data-theme='default'] re-asserts it for a reader whose system prefers dark.

Want a hook to look different in dark mode? Point it at a token, then override that token under dark. See Component hooks for the pattern.

Supporting only one theme? Put data-theme="default" or data-theme="dark" on <html> and leave it there. A named attribute at the root permanently outranks the prefers-color-scheme block, so the page stays on that theme whatever the reader's system prefers. No script needed. This only suppresses the system default at runtime; the dark CSS still ships either way.

One attribute holds one value, so themes are mutually exclusive. There is no “ocean, but dark” unless you define it. The 'ocean-dark' entry above is that definition.

Density is the one group that needs the whole page. On a section a theme only half applies. The near and away distances are computed on body, above where the section sits, so the section's own spacing keeps the page's density. A data-density-shift region inside the section does pick up the theme's value, so the result looks inconsistent rather than ignored. Put a theme that changes density on <html>, where it retunes everything.

Theme a section without a config entry

Pass an override object instead of a name. The library resolves it in the browser, then writes it to the element as inline custom properties. Reach for this when the theme comes from data: a per-tenant accent, or a color the user picked. A build-time entry is not an option there.

import { theme } from '@hyzer-labs/ui';

const warm = {
	palette: { primary: '#b45309', gray: '#78716c' },
	color: { surface: '#fffbeb', text: '#1c1917' },
	radius: { md: '0', full: '0' },     // square corners, not just new colors
	typography: { fontWeight: { medium: '700' } }
};

<section {@attach theme(warm)}>…</section>

Two trade-offs worth knowing

The resolver loads on demand, so an inline section paints unthemed for one frame. That is invisible below the fold and noticeable at the top of a page. An inline object is not contrast-graded either. The generator checks named themes against WCAG AA when it writes the sheet, and an inline object never passes through that step.

When to use a class instead

Most projects never need this

A named theme is the simpler tool, and it covers almost every case. Reach past it only when a region has to keep its own palette and still follow the page between light and dark. An entry in themes cannot do both.

The other way is to generate the sheet under a class of your own instead of :root. Scope the sheet to a class covers how. A class and data-theme are separate hooks, so a class-scoped region keeps its own palette in whichever mode the page is in.

Both bands below carry only a class. Neither sets data-theme, so both follow the page. Flip this site's light/dark toggle and watch each band change into its own dark form.

ocean

Teal on slate. Tokens only, and the reference theme does the rest.

terminal

Phosphor on black, in its own mono. No reference theme underneath it.

# A class scope composes with dark mode, which a themes entry cannot:
hyzer generate --mode overrides --selector .theme-ocean

<div class="theme-ocean">        <!-- ocean, in whichever mode is active -->
	<section data-theme="dark">…</section>
</div>

Each of those sheets names a dark theme of its own. That is what gives it a second form to switch into. Put the same theme in themes instead and the attribute is spent on the name. The region then looks the way you defined it, in both modes.