Navigation
Theme Switcher
Light, dark or system, remembered
detent-theme-switcher
Port status
- Catalog status
- implemented
- Origin
- native-only
- Phase
- 6
- Wave
- native-only
- Group
- Navigation
4.5 KB gzip for the class module
Variants
Example control: choose Light, Dark or System. This demo scopes the
result to the preview panel with the property-only target, so it never overrides the site's own theme
choice; without a target the element applies color-scheme and data-detent-theme to
document.documentElement. The choice persists under storage-key and is announced politely.
System follows prefers-color-scheme live. One detent-theme-change event
fires per user change; a programmatic value write emits nothing.
Recorded states
checkedrecorded state; no setterfocus-visiblerecorded state; no setterinvalidrecorded state; no setter
Usage
Import
import '/components/theme-switcher/define.js';
// or register it yourself
import { DetentThemeSwitcher } from '/components/theme-switcher/theme-switcher.js';
Attributes
valuelabelstorage-keypersist
Properties
valuelabelstorageKeypersisttargetresolved
Slots
No slots are recorded for this component.
Events
detent-theme-change— { theme: 'light' | 'dark' | 'system'; resolved: 'light' | 'dark' }
Example
Runnable example: site/examples/theme-switcher.ts. The snippet below is generated from the catalogued attributes.
<detent-theme-switcher
value="system"
label="Color theme"
storage-key="detent-theme"
persist
></detent-theme-switcher>
Property-only values
const element = document.querySelector('detent-theme-switcher');
if (element !== null) {
// target: HTMLElement
element.target = undefined;
// resolved: 'light' | 'dark'
element.resolved = undefined;
}
Source
Native-only row: there is no upstream source. The native module lives at:
src/components/theme-switcher/theme-switcher.ts
Native files
The native module and its artifacts:
src/components/theme-switcher/theme-switcher.tssrc/components/theme-switcher/styles.csssrc/components/theme-switcher/define.tsdocs/components/theme-switcher.mdsite/examples/theme-switcher.tstests/browser/theme-switcher.spec.tsevidence/components/theme-switcher.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
value | value | 'light' | 'dark' | 'system' | 'system' | Kebab-case attribute; camelCase property. |
label | label | string | 'Color theme' | Kebab-case attribute; camelCase property. |
storage-key | storageKey | string | 'detent-theme' | Kebab-case attribute; camelCase property. |
persist | persist | boolean | true | Presence semantics: present means true, absent means false. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
value | 'light' | 'dark' | 'system' | 'system' | Property-only: assign from script; not accepted as an attribute. |
label | string | 'Color theme' | Property-only: assign from script; not accepted as an attribute. |
storageKey | string | 'detent-theme' | Property-only: assign from script; not accepted as an attribute. |
persist | boolean | true | Property-only: assign from script; not accepted as an attribute. |
target | HTMLElement | document.documentElement | Property-only: assign from script; not accepted as an attribute. |
resolved | 'light' | 'dark' | from matchMedia | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-theme-change | (native) | (theme, resolved) => void | Composed, bubbles; fires only for a user action. |
Slots
No slots are recorded for this component.
CSS customisation
- Native-only row: no upstream source file, no upstream route (docs/migration/native-only.md).
- Three-way choice with persistence; the set only has binary toggles.
- Persistence is best-effort: storage failures fall back to in-memory state without throwing.
- Applies color-scheme plus data-detent-theme="light|dark" carrying the resolved theme; the chosen value stays on the element value attribute, so no third attribute is invented.
- A stored choice is restored only when no explicit value was supplied; an explicit value always wins.
- An invalid value resolves to system, sets aria-invalid together with data-invalid (the recorded invalid state) and logs one diagnostic.
- Target state written by the element is removed from a previous target when target changes, and deliberately left in place on disconnection to avoid a palette flash.
What it does
Rationale. A three-way theme choice with persistence, a live system mode, a radiogroup keyboard model and a polite announcement of the resolved theme.
Upstream behaviour
- Three explicit choices: light, dark and system
- Applies color-scheme and a data attribute to the target element
- Persists the choice under storage-key and restores it on connect
- System mode follows prefers-color-scheme live
- Radiogroup keyboard model with arrows, Home and End
- Announces the resolved theme in a polite live region
- One event per user change, none for programmatic writes, reduced motion honoured
Differences from upstream
- Native-only row: no upstream source file, no upstream route (docs/migration/native-only.md).
- Three-way choice with persistence; the set only has binary toggles.
- Persistence is best-effort: storage failures fall back to in-memory state without throwing.
- Applies color-scheme plus data-detent-theme="light|dark" carrying the resolved theme; the chosen value stays on the element value attribute, so no third attribute is invented.
- A stored choice is restored only when no explicit value was supplied; an explicit value always wins.
- An invalid value resolves to system, sets aria-invalid together with data-invalid (the recorded invalid state) and logs one diagnostic.
- Target state written by the element is removed from a previous target when target changes, and deliberately left in place on disconnection to avoid a palette flash.