Skip to content

Theme Toggler

A light, dark and system theme switch, as a three-option button group or a single cycling button, with a small animated icon change. It only reports the choice; it can optionally set data-theme on the page root. It reads and writes no storage: saving the choice is up to your app.

Motionv0.1.0experimentalWCAG 2.2 AAView spec

Optional, and kept apart.

Nothing here is included in a project unless you add it, and the plain component it builds on is unchanged. This effect is decoration: it is hidden from screen readers, stands still for visitors who prefer reduced motion, and can be paused. Use Still on a preview to see that version.

Chosen: system

import { useState } from "react";
import { ThemeToggler } from "@rdloom/react";

// Controlled: the page owns the choice. Nothing is stored for you; save it wherever your app keeps settings.
export default function ThemeTogglerBasicExample() {
  const [theme, setTheme] = useState<"light" | "dark" | "system">("system");
  return (
    <div className="flex w-full flex-col items-center gap-3">
      <ThemeToggler value={theme} onValueChange={setTheme} />
      <p className="m-0 text-sm text-[var(--rd-color-text-muted)]">Chosen: {theme}</p>
    </div>
  );
}

Installation

npx rdloom add theme-toggler

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes.

It also adds rdloom-motion.css next to your tokens and imports it from the tokens file, once: no setup, and no inline styles. The plain components you already use are untouched.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/theme-toggler.json

Works, but without upgrade tracking, and you import the motion CSS yourself.

Usage

import { ThemeToggler } from "@rdloom/react";

<div className="flex w-full flex-col items-center gap-3">
  <ThemeToggler value={theme} onValueChange={setTheme} />
  <p className="m-0 text-sm text-[var(--rd-color-text-muted)]">Chosen: {theme}</p>
</div>

Button

import { ThemeToggler } from "@rdloom/react";

// One button that cycles light, dark and system. Its name says the current choice and the next one.
export default function ThemeTogglerButtonExample() {
  return (
    <div className="flex w-full justify-center">
      <ThemeToggler mode="button" defaultValue="light" />
    </div>
  );
}

Apply to page

Sets data-theme on the document root. It does not remember the choice.

import { ThemeToggler } from "@rdloom/react";

// applyToDocument sets data-theme on the page root, so this switch changes the whole docs page.
export default function ThemeTogglerApplyToPageExample() {
  return (
    <div className="flex w-full flex-col items-center gap-3">
      <ThemeToggler applyToDocument size="lg" />
      <p className="m-0 text-sm text-[var(--rd-color-text-muted)]">Sets data-theme on the document root. It does not remember the choice.</p>
    </div>
  );
}

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
value

Controlled theme choice.

"light" | "dark" | "system"none
defaultValue

Initial choice when uncontrolled.

"light" | "dark" | "system""system"
onValueChange

Called with the new choice.

(value: "light" | "dark" | "system") => voidnone
mode

group shows all three options; button cycles light, dark, system.

"group" | "button""group"
applyToDocument

Sets data-theme (light or dark) on the document root. System follows the device setting live. Nothing is stored.

booleanfalse
size

Control size.

"sm" | "md" | "lg""md"
label

Accessible name of the group or button.

string"Theme"
isStill

Turns off the icon animation.

booleanfalse

Accessibility

Role radiogroup, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab: moves to the control
  • Arrow keys: move between light, dark and system in the group
  • Enter or Space: chooses the option, or cycles in button mode

Screen readers announce

  • Group named Theme with Light, Dark and System buttons and which is pressed; in button mode, the current theme and the next one

What your code must do

  • Group mode is a toggle button group with one tab stop and arrow keys; every option has a text name (Light, Dark, System) and icons are decoration
  • Button mode names the current choice and what pressing does, for example Theme: light. Switch to dark
  • Always one option is chosen; the current choice is exposed as checked state (a radio group), not by icon or colour alone
  • The icon animation is short, plays once per change, and is off for prefers-reduced-motion, rdm-still and isStill
  • Reads and writes no storage and no cookies; applyToDocument only sets one attribute on the root

Guidelines

Use it when

  • A site or app header that offers a manual theme
  • Settings screens

Avoid it when

  • When the app has a single theme

Don't

  • Relying on the icon alone to show the state
  • Storing the choice without telling the user in your privacy notes

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-border-default
  • --rd-color-surface-default
  • --rd-color-surface-subtle
  • --rd-color-focus-ring
  • --rd-radius-control