# Theme Toggle · Base UI

Switch between light and dark with an animated transition.

## Preview

Use the live preview to try the control with your theme provider.

## Examples

### Theme transition

A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.

```tsx
'use client';

import { UIThemeProvider, ThemeAnimationType } from 'uitheme-web/react';
import { ThemeToggle } from '@/components/ui/theme-toggle';

export function Example() {
  return (
    <UIThemeProvider>
      <ThemeToggle slideDirection="right" animationType={ThemeAnimationType.SLIDE} duration={500} />
    </UIThemeProvider>
  );
}
```

### Shadow borders

Use a prominent ambient shadow and inset edge in place of a solid border.

```tsx
'use client';

import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeToggle } from '@/components/ui/theme-toggle';

export function Example() {
  return (
    <UIThemeProvider>
      <ThemeToggle borderStyle="shadow" />
    </UIThemeProvider>
  );
}
```

### Icon with text

Show an icon beside the theme label.

```tsx
'use client';

import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeToggle } from '@/components/ui/theme-toggle';

export function Example() {
  return (
    <UIThemeProvider>
      <ThemeToggle display="icon-text" />
    </UIThemeProvider>
  );
}
```

## When to use

- Offer a compact light/dark action in a header, toolbar, or account menu.
- Start an animated appearance change from the control that the person presses.

## When not to use

- People need to choose or return to the system preference. Use Theme Selector.
- People are choosing a color palette. Use Color Theme Selector.

## Installation

Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install the component.

```sh
npx uitheme-cli add theme-toggle --base base-ui --tailwind 4 --language ts
```

To install with shadcn, first [add the UI-Theme registry](/docs/installation#install-with-shadcn)
to `components.json`. The style in `components.json` selects Radix or Base UI.

```sh
npx shadcn@latest add @uitheme/theme-toggle
```

## Usage

Use the component inside your theme provider. Use import paths that match your
app’s component directory.

```tsx
'use client';

import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeToggle } from '@/components/ui/theme-toggle';

export function Example() {
  return (
    <UIThemeProvider>
      <ThemeToggle />
    </UIThemeProvider>
  );
}
```

## API reference

These props belong to the installed local control. Configure theme
options on the provider.

| Prop | Type | Behavior |
| --- | --- | --- |
| label | string | Override the readable accessible label. |
| borderStyle | 'solid' | 'shadow' | Use the default solid border or prominent shadow borders. Both use the same space and theme tokens. |
| className | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style the root and trigger parts. |
| disabled | boolean | Disable interaction. Preference controls also wait for the provider to mount. |
| animationType | ThemeAnimationType | Override the UI theme transition for this control. Defaults to the provider setting. |
| duration | number | UI theme transition duration in milliseconds. Defaults to the provider setting. |
| slideDirection | SlideDirection | Valid only with animationType set to ThemeAnimationType.SLIDE. Other animation types do not accept this prop. |
| display | 'text' | 'icon-text' | Use text by default, or an icon with text. For an icon-only action, use Icon Theme Toggle. Light uses a sun, dark uses a moon, and system uses a monitor. |

## Keyboard

| Key             | Action                                                 |
| --------------- | ------------------------------------------------------ |
| `Enter / Space` | Toggle between the resolved light and dark appearance. |

## Accessibility

- Keep a meaningful label when changing the visible content.
- The pressed state reports whether the resolved appearance is dark. The control stays disabled until the provider mounts.

## Theme behavior

- The toggle reads `resolvedTheme` and sets a light or dark preference. If the preference was `system`, the toggle replaces it.
- The clicked button sets the origin for `switchTheme`. The engine changes appearance immediately when reduced motion is enabled or transitions are unsupported.

## AI Agent notes

Use this checklist when a coding agent adds the control.

- Use `uitheme-web/react` for the runtime. Import the control from its installed local file. The runtime does not export styled controls.
- For Next.js, use `NextUIThemeProvider` from `uitheme-web/next`. For TanStack Start, follow the server bootstrap guide. Create server state for each request.



- Before installation, select the framework, primitive base, Tailwind major version, and language. Run the CLI inside the app or pass `--cwd`.
- Preview changes with `--dry-run`. Keep edited local files. Review the differences before using `--overwrite`.
- Read `resolvedTheme` to choose the opposite appearance. The `system` preference resolves to light or dark.
- Keep the existing `switchTheme` call and its element origin when you change the button.
- Check keyboard controls in the running app. Check that appearance and palette choices survive a reload. Check reduced motion.



## Source

The CLI reuses your shared `cn` utility. If it is missing, installation creates it
at the configured utils path. Existing utilities are checked before a replacement
is offered.

These are the files the CLI installs. The component source stays in your app.

### theme-toggle.tsx

```
'use client';

import * as React from 'react';
import type { Theme } from 'uitheme-web/react';
import { useUITheme } from 'uitheme-web/react';
import { cn } from '@/lib/utils';
import { SunIcon, MoonIcon } from 'lucide-react';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
import { Toggle } from '@base-ui/react/toggle';

export type ThemeToggleClasses = {
  root?: string;
  trigger?: string;
};

export type ThemeToggleProps = Omit<
  React.ComponentPropsWithoutRef<'button'>,
  'className' | 'children' | 'onClick' | 'value'
> &
  ThemeControlTransitionProps & {
    value?: string;
    children?: React.ReactNode;
    onClick?: React.MouseEventHandler<HTMLButtonElement>;
    display?: 'text' | 'icon-text';
    borderStyle?: 'solid' | 'shadow';
    className?: string;
    classes?: ThemeToggleClasses;
    label?: string;
    disabled?: boolean;
  };

export const ThemeToggle = React.forwardRef<
  HTMLButtonElement,
  ThemeToggleProps
>(function ThemeToggle(
  {
    animationType,
    duration,
    slideDirection,
    borderStyle = 'solid',
    display = 'text',
    className,
    classes,
    label,
    disabled = false,
    children,
    onClick,
    ...buttonProps
  },
  forwardedRef
) {
  const { resolvedTheme, switchTheme, mounted } = useUITheme();
  const target: Theme = resolvedTheme === 'dark' ? 'light' : 'dark';
  const Icon = resolvedTheme === 'dark' ? MoonIcon : SunIcon;

  return (
    <div className={cn('inline-flex', classes?.root, className)}>
      <Toggle
        {...buttonProps}
        ref={forwardedRef}
        type="button"
        data-border-style={borderStyle}
        aria-label={
          label ??
          (display === 'text'
            ? 'Toggle appearance'
            : `Switch to ${target} theme`)
        }
        pressed={resolvedTheme === 'dark'}
        disabled={disabled || !mounted}
        className={cn(
          'inline-flex h-10 gap-2 items-center justify-center rounded-md border border-border bg-background px-3 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50',
          'data-[border-style=shadow]:[--shadow-border-edge:theme(colors.border)] data-[border-style=shadow]:[--shadow-border-focus:theme(colors.ring)] data-[border-style=shadow]:border-transparent data-[border-style=shadow]:focus-visible:border-transparent data-[border-style=shadow]:ring-0 data-[border-style=shadow]:focus-visible:ring-0 data-[border-style=shadow]:shadow-[0_0_4px_rgb(0_0_0_/_0.25),inset_0_0_0_1px_var(--shadow-border-edge)] data-[border-style=shadow]:hover:shadow-[0_0_4px_rgb(0_0_0_/_0.25),inset_0_0_0_1px_color-mix(in_oklch,var(--shadow-border-focus)_55%,transparent)] data-[border-style=shadow]:focus-visible:shadow-[0_0_12px_color-mix(in_oklch,var(--shadow-border-focus)_18%,transparent),0_1px_3px_rgb(0_0_0_/_0.1),inset_0_0_0_2px_color-mix(in_oklch,var(--shadow-border-focus)_55%,transparent)] data-[border-style=shadow]:forced-colors:outline data-[border-style=shadow]:forced-colors:outline-1 data-[border-style=shadow]:forced-colors:outline-[CanvasText] data-[border-style=shadow]:forced-colors:focus-visible:outline-2 data-[border-style=shadow]:forced-colors:focus-visible:outline-[Highlight]',
          classes?.trigger
        )}
        onClick={(event) => {
          onClick?.(event);

          if (!event.defaultPrevented)
            void switchTheme(target, {
              element: event.currentTarget,
              animationType,
              duration,
              slideDirection,
              animationOff: event.detail === 0,
            });
        }}
      >
        {children ?? (
          <>
            {display !== 'text' && (
              <Icon
                aria-hidden="true"
                focusable="false"
                className="size-4 shrink-0"
              />
            )}
            {resolvedTheme === 'dark' ? 'Dark' : 'Light'}
          </>
        )}
      </Toggle>
    </div>
  );
});

```

## Customization

Use `borderStyle="shadow"` in React or `border-style="shadow"` in Vue for prominent shadow borders. The default is `solid`.

Use `slideDirection` only when `animationType` is `ThemeAnimationType.SLIDE`. Set both props on the control when you need a slide direction override.

Use `display="icon-text"` to show an icon beside the text. The default is `display="text"`.
For an icon-only action, use [Icon Theme Toggle](/docs/components/base-ui/icon-theme-toggle).
Pass `animationType`, `duration`, and `slideDirection` to this control to set its UI theme transition.
These props override the provider settings for this action only.

Keyboard focus shows the theme’s focus ring.

Edit the installed source. Use `className` in React or `class` in Vue to style
the root. Use the `classes` prop to style individual parts. Use label props to change the
visible text.
