block
A pre-built block for switching between light and dark. Renders a button that opens a Menu whose radio group holds two options — Dark and Light — and persists the choice to localStorage. The button icon updates automatically to reflect the active mode.
Because it’s a menu radio group, the options come with roving focus, typeahead, a keyboard-only focus ring, and a check mark on the active mode for free — and picking one doesn’t dismiss the panel, so modes can be previewed in place.
There’s deliberately no “System” row. Changing theme is a situational comfort fix — a page is too bright right now — not a standing declaration about how every future page should resolve, so asking users to reason about a third state surfaces the data model rather than the goal.
The third state still exists; it’s just reached by using the two that are visible:
prefers-color-scheme is checked — the selection is never empty.theme-mode key.An override is only ever released at interaction time. If the OS flips to match a stored choice, the choice is kept — nothing is cleared behind the user’s back.
---
import ThemeModeSelect from "../theme-mode-select.astro";
import { flex } from "@pindoba/styled-system/patterns";
---
<div
class={flex({
justify: "center",
})}
>
<ThemeModeSelect id="demo-theme-mode-select" />
</div>The menu opens below the trigger and aligned to its end edge. placement takes any Floating UI placement — useful when the switch sits at the bottom of a sidebar or footer, where a downward panel would have nowhere to go.
---
import ThemeModeSelect from "../theme-mode-select.astro";
import { flex } from "@pindoba/styled-system/patterns";
---
<div
class={flex({
justify: "center",
})}
>
<ThemeModeSelect id="demo-theme-mode-select-placement" placement="top" />
</div>Props forwarded to the trigger `<Button>`.
Root element id. Also seeds the menu, trigger, and option ids. Defaults to a generated value.
Props forwarded to the internal `<Menu>` (e.g. `size`). `passThrough` is owned by the block — use this component's own `passThrough.trigger` / `passThrough.menuPanel` instead of nesting one here.
Per-slot escape hatch. `root` accepts `style` (merged into the slot class) and `props`; `trigger` and `menuPanel` forward onto the Menu's own `passThrough.trigger` / `passThrough.popoverPanel`.
"bottom-end"Placement of the menu panel relative to its trigger.
Props forwarded to the trigger `<Button>` (e.g. `size`, `emphasis`).