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.

Naming 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.

light

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.

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>

Defining themes

Named themes come from the themes map in your config. Each entry becomes one [data-theme="…"] block in the generated sheet, graded for contrast the same way the built-in dark theme is.

// 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 is an entry like any other. It merges over what the library already authors rather than replacing it. light is reserved as a name: the default theme is the :root block you author through tokens, and [data-theme='light'] re-asserts that default for a reader whose system prefers dark.

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.

Themes without a config entry

Pass an override object instead of a name and it is resolved in the browser, then written to the element as inline custom properties. Reach for this when the theme comes from data, like a per-tenant accent or a color the user picked, where a build-time entry is not an option.

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

const warm = {
	palette: { primary: '#b45309', gray: '#78716c' },
	color: { surface: '#fffbeb', text: '#1c1917' }
};

<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: named themes are checked against WCAG AA when the sheet is generated, and an inline object never passes through that step.

When to use a class instead

Scoping a generated sheet under a class is the other way to do this, and for one case it is the better way. A class composes with data-theme, so a themed region still has a light and a dark form. A themes entry cannot, because it occupies the same attribute dark does.

// A class scope is the other mechanism, and the more expressive one:
// it COMPOSES with dark mode, which a themes entry cannot.
generateCss(resolveConfig(oceanConfig), { selector: '.theme-ocean' });

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

Reach for a themes entry when a section should look one specific way. Reach for a class when a whole region needs its own palette and still has to answer to the page's light/dark state.