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.
The default theme, restored explicitly, even inside a dark page.
Surfaces, text, borders and every intent follow the attribute.
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
<!-- 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
When to use a class instead
Most projects never need this
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.
Teal on slate. Tokens only, and the reference theme does the rest.
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.