# Theme

> A subtree in light, dark, or the opposite of its surroundings, plus the helpers that switch the page mode.

URL: https://apogee.avaui.dev/docs/theme
Last updated: 2026-10-20

A subtree in light, dark, or the opposite of its surroundings, plus the helpers that switch the page mode.

Last updated October 20, 2026

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.

```astro
<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

```sh
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

```astro
---
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`:

```astro
<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.

```astro
<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](https://apogee.avaui.dev/docs/dark-mode) covers the script and the helpers in full.

```html
<script is:inline>
  const t = localStorage.getItem('apogee-theme')
  if (t) document.documentElement.dataset.theme = t
</script>
```

```ts
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`.

```astro
<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:

```astro
<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 `inverted` for a callout that should stand out from its section without knowing the section’s mode.
-   **Don’t** nest more than one `inverted` level 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.

## Related

[

HiddenHide children below or above a breakpoint, or visually only.

](https://apogee.avaui.dev/docs/hidden)[

SectionA full-width band with vertical rhythm and an optional Container, used to lay out page content.

](https://apogee.avaui.dev/docs/section)[

ContainerCenters content at a max width with fluid side gutters.

](https://apogee.avaui.dev/docs/container)
