Skip to content
Home Theme Gallery

Theme toggle

The custom element is <hui-theme-toggle>.

Overview

An optional enhancement that writes the light or dark class on the document root and remembers the choice in localStorage. It is the only element in the library that touches the root element, so a host that pins the theme server-side simply does not load it.

Example

Cycle the theme
Source
<hui-theme-toggle>
<button type="button">Theme: <span data-theme-label>system</span></button>
</hui-theme-toggle>

The Go template that renders it:

<hui-theme-toggle>
<button type="button">Theme: <span data-theme-label>system</span></button>
</hui-theme-toggle>

API

Attributes2
Attributes
NameTypeDefaultDescription
storage-keystring"hui-theme"The localStorage key the choice is written to and read from. Read on every access, so it may change while the element is connected.
data-theme"light" | "dark" | "system""system"Reflected by the element, not authored: the current choice, kept in step with the class on <html>. Read it to drive host styling or other UI.
Properties1
Properties
NameTypeDefaultDescription
themeThemeChoice'system'Read-only. The stored choice when it is one of the three names, otherwise system. Assign through setTheme().
Methods1
Methods
NameTypeDescription
setTheme(theme: ThemeChoice) => voidWrites the choice to localStorage and applies it to <html>. Unlike a click it does not emit hui-theme-change, so a host calling it is not notified of its own change.
Events2
Events
NameDescription
hui-theme-changeFired after a click or keypress cycles the theme. It bubbles and is composed, is not cancelable, and carries detail.theme with the new "light" | "dark" | "system" value. setTheme() does not emit it.
clickThe native click from the slotted control, or from the element itself when it takes on role="button". Cycling happens on this event, in the bubble phase.
Slots1
Slots
NameDescription
(default)A server-rendered <button>, <a> or [role="button"], optionally with a [data-theme-label] child that the element keeps in sync with the theme name. When no such control is present the element becomes one itself.

States

No child control

With no child, the element renders its own button.

No child control
Source
<hui-theme-toggle></hui-theme-toggle>

Custom storage key

A host's own button as the trigger, data-theme-label filled in with the current theme, and storage-key naming where the preference is kept.

Custom storage key
Source
<hui-theme-toggle storage-key="demo-theme">
<button type="button">Theme: <span data-theme-label>system</span></button>
</hui-theme-toggle>

Server-side mechanics

The server owns the theme: it stamps the light or dark class on <html> from the cookie or user setting before first paint, and that class is the theme of record. On connect the element reads the class already present and reflects it as data-theme, so a host that pins the theme server-side simply does not load it - a click would write a class that disagrees with the server, and the element persists its own choice in localStorage. It is not form-associated and submits no value; there is no name. An hx-swap that replaces the markup creates a fresh element, which re-registers its click and keydown listeners and re-reads the stored choice, or the reflected class, on connect.

Accessibility

  • The element sets aria-label="Switch theme, current <theme>" on itself and on a slotted control, so the current choice is part of the control name.
  • With no slotted control it adds role="button" and tabindex="0", and handles Enter and Space as a native button would.
  • The choice is reflected as the light or dark class on <html> and as data-theme, so host CSS and assistive tech can read the current state.
  • Storage access is wrapped in a try/catch: a browser that blocks localStorage still toggles for the session, it just cannot persist.

Keyboard

Keyboard
KeysAction
TabMoves focus to the slotted control, or to the element when it supplies its own button role.
Enter, SpaceCycles the theme when the element carries role="button"; otherwise the slotted native control activates as usual.

Gotchas

  • It sets no aria-pressed. The current choice is exposed through aria-label="Switch theme, current <theme>" and the reflected data-theme, so do not look for a pressed state.
  • setTheme() writes storage and <html> but does not emit hui-theme-change; only a click or keyboard cycle does, so a host calling it is not notified of its own change.
  • It consumes no --hui-* token directly; it only writes the light or dark class that the token sheet keys off.