Theme Switcher
nswds-ui/theme-switcherFreeComponent
Light/dark toggle button with controlled and uncontrolled modes — framework-free, wire it to next-themes or any theme store.
Install it
npx shadcn@latest add https://raw.githubusercontent.com/digitalnsw/nswds-ui/main/apps/registry/public/r/theme-switcher.jsonSource
1'use client'23import React from 'react'45import { IconDarkMode } from '@/icons/dark-mode'6import { IconLightMode } from '@/icons/light-mode'78import { Button, type ButtonProps } from '@/components/button'910/** The two colour schemes the switcher toggles between. */11type ThemeSwitcherTheme = 'light' | 'dark'1213type ThemeSwitcherProps = Omit<ButtonProps, 'children'> & {14 /**15 * Current theme (controlled). When set, the component never updates its own16 * state — wire `onThemeChange` back into whatever owns the theme (see the17 * next-themes snippet on the Storybook docs page). Pick one mode per18 * instance: don't switch between supplying and omitting `theme` across19 * renders.20 */21 theme?: ThemeSwitcherTheme22 /** Initial theme when uncontrolled. Defaults to `'light'`. */23 defaultTheme?: ThemeSwitcherTheme24 /**25 * Called with the NEXT theme (the one the user asked for) on every26 * activation, in both controlled and uncontrolled modes.27 */28 onThemeChange?: (theme: ThemeSwitcherTheme) => void29}3031/**32 * Light/dark theme toggle button, ported from nswds-app's `ThemeSwitcher`33 * minus its `next-themes` dependency — a design system cannot depend on a34 * theming framework, so this is a plain controlled/uncontrolled input35 * (`theme` / `defaultTheme` / `onThemeChange`) and the app owns the plumbing.36 * All other props pass through to `Button` (defaults: `variant='surface'`,37 * `color='grey'`, `size='icon'` — the source's `color='light'` does not exist38 * on this Button; `grey` is its nearest ink).39 *40 * The icon depicts the DESTINATION: a moon (`IconDarkMode`) while the theme is41 * light, a sun (`IconLightMode`) while it is dark — matching the source and42 * the action-phrased label below.43 *44 * Accessibility contract:45 * - The `aria-label` announces the ACTION ("Switch to dark theme"), not the46 * state, and flips after each activation so the control always names what47 * pressing it will do (WCAG 2.2, 4.1.2 Name, Role, Value). The source48 * duplicated the same words in an `sr-only` span AND the `aria-label`; the49 * span is dropped here because a control with both gets its name announced50 * twice by common screen readers (label, then descendant text).51 * - No `aria-pressed`. This is a mode switch between two peer states, each52 * with its own action label — not a pressed/unpressed toggle of a single53 * action. Combining `aria-pressed` with a flipping label is actively54// … truncated
What it pulls in
Other registry items
Files it writes
More from @nswds/ui
All 46 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aspect Ratioaspect-ratio | @nswds/ui | Components | Free | 2 deps · 2 files | |
| Badgebadge | @nswds/ui | Components | Free | 4 deps · 2 files | |
| Buttonbutton | @nswds/ui | Components | Free | 4 deps · 2 files | |
| Cardcard | @nswds/ui | Components | Free | 2 deps · 2 files | |
| Collapsiblecollapsible | @nswds/ui | Components | Free | 1 dep | |
| Drawerdrawer | @nswds/ui | Components | Free | 3 deps · 2 files | |
| Expandable Searchexpandable-search | @nswds/ui | Components | Free | 3 deps · 2 files | |
| Fieldfield | @nswds/ui | Components | Free | 4 deps · 2 files |
