Actions

ColorSchemeToggle

The control that switches the app between light, dark and the system's scheme, and keeps the choice. variant="menu", the default, is an icon button for a header: the sun or the moon of the scheme the page shows, opening a menu of Light, Dark and System, the current one checked. variant="segmented" shows the three side by side, for a settings page. Either way the choice applies at once, as the .dark class on <html> that shadcn themes switch on, and is kept in localStorage.

Appearance

In Spoonjoy

Spoonjoy, a recipe app, is built with Ouro HX (how).

See it live, no sign-in needed: Home · A recipe · Recipes

In its source: app/root.tsx

Install

Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):

npx shadcn@latest init --base base --preset nova --yes --no-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):

npx shadcn@latest add ourostack/ouro-ui/color-scheme-toggle#v1.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/color-scheme-toggle

Its tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):

npx shadcn@latest add ourostack/ouro-ui/color-scheme-toggle#v1.2.13 ourostack/ouro-ui/color-scheme-toggle-tests#v1.2.13
import { ColorSchemeToggle } from "@/components/ouro/color-scheme-toggle/color-scheme-toggle";

Paths use shadcn's default @/ alias; the CLI rewrites them to yours.

Example

<Navbar label="Main" items={items} actions={<ColorSchemeToggle />} />
<ColorSchemeToggle variant="segmented" label="Appearance" />

Usage

It works on its own, but the choice then shows only once the toggle changes it: wrap the app in ColorSchemeProvider, which applies the kept choice on every page and follows the system's setting live, and put colorSchemeScript() in <head> so a reload never flashes the wrong scheme. For a control of your own, useColorScheme() gives the scheme and its setter.

Use instead when

  • Use Switch with useColorScheme() instead for a plain dark-mode on/off, and SegmentedControl for other choices of how a view looks.

Props

Type: ColorSchemeToggleProps.

className
Type: string
Layout only: margins, grid placement (ms-auto). Appearance classes warn in development.
label
Type: string
Default: "Color scheme"
Names the control for screen readers: the button's name, or the segmented control's label.
labels
Type: ColorSchemeLabels
The words for each scheme.
ref
Type: Ref<HTMLElement>
Reaches the menu button, or the segmented control's group.
variant
Type: "menu" | "segmented"
Default: "menu"
menu is an icon button, the sun or the moon of the scheme the page shows, that opens a menu of Light, Dark and System, for a header. segmented shows the three side by side, for a settings page.

Don't use

These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.

  • onChange: Not a prop. The toggle applies the choice itself; to react to it, read useColorScheme().resolved.
  • theme: Not a prop. Ouro UI says color scheme: <ColorSchemeToggle /> with ColorSchemeProvider around the app.
  • value: Not a prop. The toggle keeps the scheme itself, through ColorSchemeProvider; read it with useColorScheme().

className is for layout only

Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: ColorSchemeToggle merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes

Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.

Alignment classes (text-center) warn too: set alignment on a parent element.

Types

ColorSchemeLabels

The words for each scheme, for apps in other languages.

interface ColorSchemeLabels {
  /** @default "Light" */
  light?: string | undefined;
  /** @default "Dark" */
  dark?: string | undefined;
  /** @default "System" */
  system?: string | undefined;
}

colorSchemeScript()

The script that sets the page's color scheme before it paints, so a reload in dark mode never flashes light. It reads the choice ColorSchemeProvider keeps in localStorage, resolves system through prefers-color-scheme, and sets the .dark class and the color-scheme property on <html>. Put it in <head> as an inline script, with the same options as the provider, and add suppressHydrationWarning to <html>, whose class the script changes before React hydrates. It is plain text, so a Server Component can call it.

import { colorSchemeScript } from "@/lib/ouro/color-scheme-script";

Example

<html lang="en" suppressHydrationWarning>
  <head>
    <script dangerouslySetInnerHTML={{ __html: colorSchemeScript() }} />
  </head>
  <body>
    <ColorSchemeProvider>{children}</ColorSchemeProvider>
  </body>
</html>

Parameters

options
Type: ColorSchemeOptions
{}

Returns

string

useColorScheme()

The person's color scheme, the scheme the page shows, and the setter that changes both, for a control of your own. ColorSchemeToggle is that control, ready made.

import { useColorScheme } from "@/lib/ouro/color-scheme";

Example

const { resolved, setScheme } = useColorScheme();
<Switch label="Dark mode" checked={resolved === "dark"} onCheckedChange={(on) => setScheme(on ? "dark" : "light")} />

Usage

It works with or without ColorSchemeProvider, with the provider's options when there is one. Before hydration it reports the default scheme, as the server did.

Returns

ColorSchemeValue

Types

ColorSchemeValue

What useColorScheme returns.

interface ColorSchemeValue {
  /** The person's choice: `light`, `dark` or `system`. */
  scheme: ColorScheme;
  /** The scheme the page shows: `system` resolved through the system's setting. */
  resolved: ResolvedColorScheme;
  /** Keeps a new choice and applies it to the page at once. */
  setScheme: (scheme: ColorScheme) => void;
}

Claim sources