Theme
A subtree in light, dark, or the opposite of its surroundings, plus the helpers that switch the page mode.
Last updated
Uses JavaScript
Resolves mode="inverted": a ten-line script reads the nearest light or dark
ancestor and sets the opposite, at page load and after every setTheme().
Without JS: light and dark are pure CSS and keep working. An inverted region renders in its parent's mode until the script runs.
Dark region
Inverted: light inside dark
Inverted again: dark
Show codeHide code
<Theme mode="dark" class="rounded-lg p-4">
<p>Dark region</p>
<Theme mode="inverted" class="mt-3 rounded-md p-3">
<p>Inverted: light inside dark</p>
<Theme mode="inverted" class="mt-3 rounded-md p-3">
<p>Inverted again: dark</p>
</Theme>
</Theme>
</Theme>Add
npx @ava-ui/apogee add theme
Depends on utils (theme.ts). The page-level mode (<html data-theme>) needs
the head script below; Theme only scopes a subtree.
Usage
---
import Theme from '@/components/apogee/Theme/Theme.astro'
---
<Theme mode="dark" class="rounded-lg p-6">
…a dark card on a light page…
</Theme>
API
@composable — exposed to Content Editors in AVA. Any other attribute passes
through to the wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
mode |
'light' | 'dark' | 'inverted' |
— | The subtree’s colour mode. inverted is the opposite of the nearest themed ancestor (or the page). |
as |
string |
'div' |
Wrapper element. |
class |
string |
— | Extra classes, merged last. The wrapper already sets the page background and text colour. |
Modes
Every mode:
Show codeHide code
<Theme mode="light" class="rounded-lg border border-neutral p-4">Light</Theme>
<Theme mode="dark" class="rounded-lg p-4">Dark</Theme>
<Theme mode="inverted" class="rounded-lg border border-neutral p-4">Inverted</Theme>Nesting
An inverted region reads its nearest light or dark ancestor, so the same markup flips both ways.
Dark
Inverted → light
Light
Inverted → dark
Show codeHide code
<Theme mode="dark"><Theme mode="inverted">…</Theme></Theme>
<Theme mode="light"><Theme mode="inverted">…</Theme></Theme>Page mode and the toggle
The page mode lives on <html data-theme>. With no attribute the page follows
the system setting. To restore a stored choice, set it before paint with a head
script, then switch it with the helpers in utils/theme.ts. Dark mode covers the script and the helpers in full.
<script is:inline>
const t = localStorage.getItem('apogee-theme')
if (t) document.documentElement.dataset.theme = t
</script>
import { getTheme, setTheme, toggleTheme } from '@/components/apogee/utils/theme.ts'
toggleTheme() // light ⇄ dark, persisted, no transition flash
setTheme('dark', { persist: false })
getTheme() // 'light' | 'dark'
setTheme adds apogee-no-transitions to <html> for two frames so the switch
does not tween every colour, re-resolves inverted regions, and dispatches
apogee:theme-change (detail: { mode }) on document.
Show codeHide code
<button onclick="toggleTheme()">Toggle theme</button>States
- Light / dark — the two generated token sets; pure CSS, nestable.
- Inverted — resolved to one of the two by the island; until then the
element carries
data-theme="inverted"and inherits its parent.
Examples
A dark hero band on a light page. Everything inside, Button included, reads the dark tokens:
Ship your docs today
Copy the components in, keep the code.
Show codeHide code
<Theme mode="dark" as="section" class="rounded-lg p-8">
<Heading as="h3" size="h3">Ship your docs today</Heading>
<p class="mt-2 text-neutral-faded">Copy the components in, keep the code.</p>
<Button class="mt-4">Get started</Button>
</Theme>Accessibility
- A themed region keeps every contrast pair of its mode (the tokens are the same ones the contrast gate checks).
- Switching the page mode does not move focus or announce anything; pair the
toggle with a visible label or
aria-label.
Tokens
| Token | Used for |
|---|---|
--background-page / --foreground-neutral |
The wrapper’s surface and text |
every [data-theme] token |
Re-resolved inside the region |
Do / Don’t
- Do use
invertedfor a callout that should stand out from its section without knowing the section’s mode. - Don’t nest more than one
invertedlevel for effect; two flips are the parent’s mode again. - Don’t put the page toggle inside an inverted region and expect it to
read the region’s mode: it always reads
<html>. - Don’t rely on the
dark:utility variant inside a light or inverted region nested in a dark one: the variant is ancestor-based, so it still applies there. Tokens (bg-page,text-neutral) follow the region; the variant does not.
Content
- No wording rules of its own. Text inside a themed region follows the rules of the components it holds.