block

Theme Mode Select

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.

Two options, three states

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:

  • The checked row is the mode on screen, not the stored preference. With nothing saved, the row matching the OS prefers-color-scheme is checked — the selection is never empty.
  • Picking the other mode stores an override under the theme-mode key.
  • Picking the mode the OS already prefers clears that key, so the page follows the OS again. Every choice stays reversible with one more click.

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.

Default

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

Placement

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 · 6 shown · 6 total
buttonProps deprecated
Record<string, unknown>

Props forwarded to the trigger `<Button>`.

id
string

Root element id. Also seeds the menu, trigger, and option ids. Defaults to a generated value.

menuProps
Record<string, unknown>

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.

passThrough
ThemeModeSelectPassThrough

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

placement
"top""right""bottom""left""top-start""top-end""right-start""right-end""bottom-start""bottom-end""left-start""left-end"
default "bottom-end"

Placement of the menu panel relative to its trigger.

triggerProps
Record<string, unknown>

Props forwarded to the trigger `<Button>` (e.g. `size`, `emphasis`).

Type

  • Components
  • Blocks