# Introduction
Install the runtime once. Choose the import for your framework.
UI-Theme manages appearance, palettes, persistence, and animated theme transitions.
Styled controls are source files that you install separately.
```sh
npm install uitheme-web@latest
```
| Environment | Import |
| -------------------------------- | ----------------------------------- |
| Framework-neutral browser engine | `uitheme-web` or `uitheme-web/core` |
| React | `uitheme-web/react` |
| Next.js client provider | `uitheme-web/next` |
| Next.js server bootstrap | `uitheme-web/next/server` |
| TanStack Start server helpers | `uitheme-web/tanstack` |
| Vue 3 | `uitheme-web/vue` |
| Vue server bootstrap | `uitheme-web/vue/bootstrap` |
| Nuxt 4 module | `uitheme-web/nuxt` |
The runtime does not depend on UI primitives. Framework dependencies are optional peer dependencies.
Install React or Vue in the app that uses that adapter.
# Installation
## Choose an install path [#choose-an-install-path]
Each path uses the `uitheme-web` runtime. Pick one.
| Path | Use it when |
| ------------------------------------- | -------------------------------------------------------------------------------- |
| Runtime only | You build your own controls with the hooks. Any CSS works, Tailwind is optional. |
| [UI-Theme CLI](#install-with-the-cli) | You want ready controls in React or Vue, with Tailwind 3.4 or 4. |
| [shadcn](#install-with-shadcn) | Your React app already uses shadcn with Tailwind 4. |
For the runtime only, install the package and add the provider for your
framework:
```sh
npm install uitheme-web@latest
```
The runtime changes theme classes and CSS variables. Supply your own styles.
The CLI and shadcn copy editable [controls](/docs/components) into your app.
The CLI requires an existing Tailwind setup. It does not create an app or
upgrade Tailwind.
## React [#react]
[Follow the React guide](/docs/guides/react).
```tsx
import { UIThemeProvider } from 'uitheme-web/react';
export function App() {
return (
);
}
```
Read state and call actions with `useUITheme()` from `uitheme-web/react`.
`setTheme` and `setColorTheme` update immediately.
`switchTheme` and `switchColorTheme` use an animated transition.
Pass `{ element: event.currentTarget }` to anchor an animation to a control.
`theme` stores the preference. `resolvedTheme` reports light or dark after
resolving the system preference.
## Next.js App Router [#nextjs-app-router]
[Follow the Next.js guide](/docs/guides/nextjs).
Create a client provider wrapper.
```tsx title="app/providers.tsx"
'use client';
import { NextUIThemeProvider } from 'uitheme-web/next';
export function Providers({ children }: { children: React.ReactNode }) {
return {children};
}
```
Render `Providers` inside the root layout's body. Add `suppressHydrationWarning`
to the `html` element. The provider includes its pre-hydration bootstrap.
For a custom server bootstrap, import server-safe helpers from
`uitheme-web/next/server`. Keep browser hooks inside client components.
## TanStack Start [#tanstack-start]
[Follow the TanStack Start guide](/docs/guides/tanstack-start).
Use `TanStackUIThemeProvider` from `uitheme-web/react` for client state.
Use the stateless helpers from `uitheme-web/tanstack` to read server theme data
and build the initial HTML attributes and bootstrap script.
Create server data per request. Pass matching initial state to the provider.
Connect theme and palette change callbacks to your cookie persistence.
Keep storage keys and available options identical on the server and client.
## Vue 3 [#vue-3]
[Follow the Vue guide](/docs/guides/vue).
```ts title="src/main.ts"
import { createApp } from 'vue';
import { createUITheme } from 'uitheme-web/vue';
import App from './App.vue';
createApp(App)
.use(
createUITheme({
defaultTheme: 'system',
colorThemes: ['default', 'caffeine'],
})
)
.mount('#app');
```
Use `useUITheme()` from `uitheme-web/vue` in setup.
A `UIThemeProvider` can define a separate scope.
For server rendering, create a fresh app and plugin for each request.
Use `getThemeScript` from `uitheme-web/vue/bootstrap` before hydration.
Do not share a provider or a theme store between requests.
## Nuxt 4 [#nuxt-4]
[Follow the Nuxt guide](/docs/guides/nuxt).
```ts title="nuxt.config.ts"
export default defineNuxtConfig({
modules: ['uitheme-web/nuxt'],
uiTheme: {
defaultTheme: 'system',
colorThemes: ['default', 'caffeine'],
},
});
```
The module configures the Vue plugin, server bootstrap, and cookie persistence.
Use `useUITheme()` or import it from `uitheme-web/vue`.
Automatic setup supports Nuxt 4. The CLI rejects Nuxt 3 before changing files.
## Tailwind and palettes [#tailwind-and-palettes]
Use the dark variant for the `.dark` class in your Tailwind configuration.
For Tailwind 4, this can be defined in your CSS:
```css
@custom-variant dark (&:where(.dark, .dark *));
```
For Tailwind 3.4, set `darkMode: 'class'` in the Tailwind configuration.
Keep the existing content paths and include the directory for installed controls.
Define the semantic tokens used by controls, including `background`, `foreground`,
`primary`, `accent`, `muted`, `border`, `input`, `ring`, and `popover`, with their
foreground counterparts. Palette classes use the `theme-` prefix by default.
The CLI supports complete CSS colors and HSL channel tokens.
Review the dry-run before applying CSS changes. Existing imports, variables,
and plugins must remain intact.
## Without Tailwind [#without-tailwind]
The runtime works with any CSS. It adds the `dark` class to the `html` element
for the dark appearance. It also adds a `theme-` class for the selected
palette, for example `theme-ocean`. Register each palette key in `colorThemes`
on the provider. Then define your variables for these classes:
```css title="styles.css"
:root {
--background: #ffffff;
--foreground: #0a0a0a;
}
.dark {
--background: #0a0a0a;
--foreground: #fafafa;
}
.theme-ocean {
--background: #f0f9ff;
}
.theme-ocean.dark {
--background: #082f49;
}
body {
background: var(--background);
color: var(--foreground);
}
```
The CLI and shadcn controls use Tailwind classes. Without Tailwind, build your
controls with `useUITheme()`.
## Install with the CLI [#install-with-the-cli]
Use the [component catalog](/docs/components) to choose a control. The CLI copies
editable source into your app. Set up the runtime, provider, and Tailwind first.
React supports Base UI (the default) and Radix. Vue supports Reka only.
Each installed component uses one base. It does not switch bases at runtime.
```sh
npx uitheme-cli init
npx uitheme-cli add theme-toggle --base base-ui
```
See the [CLI guide](/docs/cli) for commands, flags, workspace paths, and previews.
Review the plan before you replace edited component files.
## Install with shadcn [#install-with-shadcn]
Use this path in a React app that already has shadcn and Tailwind 4.
1. Add the UI-Theme registry to `components.json`:
```json title="components.json"
{
"registries": {
"@uitheme": "https://www.ui-theme.dev/r/{style}/{name}.json"
}
}
```
2. Add a control:
```sh
npx shadcn@latest add @uitheme/theme-toggle
```
3. Add the provider for your framework, as the sections above show. shadcn
installs `uitheme-web`, but it does not add the provider.
The `style` in `components.json` selects the primitive base:
| Styles | Base |
| --------------------------------------------------- | ------- |
| `radix-*`, `new-york`, `new-york-v4`, and `default` | Radix |
| `base-*` | Base UI |
The registry serves Tailwind 4 source only. React Aria styles (`aria-*`) are
not supported. For Vue or Tailwind 3.4, use the
[UI-Theme CLI](#install-with-the-cli).
shadcn does not write `uitheme.json`, so `uitheme-cli diff` does not track the
files that shadcn installs.
## Native applications [#native-applications]
For Expo and React Native, install `uitheme-rn`. Follow the [Expo guide](/docs/guides/expo), [NativeWind guide](/docs/guides/nativewind), or [Uniwind guide](/docs/guides/uniwind). Each guide links to its example application.
# CLI
The `uitheme-cli` package provides the `uitheme` command. Run it with your package
runner. Use Node.js 22.18 or later and an existing app with Tailwind CSS 3.4 or 4.
See [Installation](/docs/installation) for runtime and provider setup.
The CLI supports Next.js App Router, TanStack Start, Vite React, Vite Vue, and
Nuxt 4. Registry controls require `uitheme-web` 2.x. Install the current runtime
with `npm install uitheme-web@latest`.
## info [#info]
Show the detected framework, language, package manager, Tailwind version,
component base, and file paths. This command prints JSON and does not change files.
```sh
npx uitheme-cli info
npx uitheme-cli info --cwd apps/web
```
Use the [project options](#project-options) to resolve an ambiguous result.
The CLI reads app configuration without executing it.
## init [#init]
Set up the framework provider, theme CSS, and `uitheme.json` configuration.
For React, the CLI creates `theme-provider.tsx` (or `.jsx`) one folder above the
installed controls, for example `src/components/theme-provider.tsx`. It exports a
`ThemeProvider` that wraps `UIThemeProvider`.
In a Vite entry, the CLI places `ThemeProvider` directly inside
``, around your other providers. If your app already has a
provider file or a `cn` utility that differs, the CLI asks before it replaces
the file. If you keep your file, the CLI prints the code to add by hand.
The first installation creates a shared `cn` utility when it is missing. The CLI
reuses the utils alias in `components.json`, or uses `lib/utils` in the source
folder. Later installs reuse it and repair a missing file. A valid callable `cn`
export is preserved, including when `--overwrite` is set. If validation cannot
resolve an import, fix that import before replacing the file.
Review the plan before applying changes.
```sh
npx uitheme-cli init --dry-run
npx uitheme-cli init
```
For React, the CLI selects the base that the app already uses. It selects Radix
when the app depends on `radix-ui` or `@radix-ui/*`, and Base UI when the app
depends on `@base-ui/react`. If the app has both or neither, the shadcn style in
`components.json` decides: `base-*` styles select Base UI, and other styles
select Radix. Otherwise, the CLI uses Base UI. Set `--base` to choose another
base. Vue uses Reka.
Interactive runs can ask for a framework or component base when a choice is needed.
For unattended runs, provide explicit choices and `--yes`.
```sh
npx uitheme-cli init --cwd apps/web --base radix --yes
```
The CLI does not create an app or upgrade Tailwind. It preserves existing source
and reports setup steps that need manual changes.
## list [#list]
List the available components for the detected or selected base, Tailwind major,
and source language. This command reads the registry and does not install files.
```sh
npx uitheme-cli list
npx uitheme-cli list --base radix --language js --json
```
JSON output contains each component's name, title, and description.
Use `--revision` to inspect a recorded registry revision.
## add [#add]
Install one or more components and their required helpers and dependencies.
The source belongs to your app after installation.
```sh
npx uitheme-cli add theme-toggle theme-selector --dry-run
npx uitheme-cli add theme-toggle theme-selector
```
Select the output when the detected defaults do not match your app:
```sh
npx uitheme-cli add color-theme-selector --base radix --tailwind 4 --language ts
```
The CLI uses one immutable registry revision for the component and its dependencies.
It records file hashes in `uitheme.json` to detect later local edits.
When a file differs from the uitheme version, the result depends on the run:
- In a terminal, the CLI asks for each file: Keep my file (the default),
Overwrite, or Show the diff.
- With `--yes`, or in CI, the CLI keeps your files, lists them, and exits with
code 2.
- With `--overwrite`, the CLI replaces the files without a question.
- With `--dry-run`, the plan shows each file as
`keep (...; --overwrite replaces it)`.
An unedited control from an older revision updates without a question. Its hash
shows that you did not change it.
If the app has no `uitheme.json`, `add` asks whether to run `init` first. Without
a terminal, `add` stops and tells you to run `uitheme init`.
```sh
npx uitheme-cli add theme-toggle --overwrite --dry-run
npx uitheme-cli add theme-toggle --overwrite --yes
```
The CLI keeps a dependency that your app already has when its version range
is compatible with the controls. Otherwise, the plan shows an update, for example
`update lucide-react ^0.300.0 -> ^1.61.0`. A major update has a note, because it
can change other imports from that package in your app. Review the plan before
you confirm it. An older `uitheme-web` major is not updated; run
[`migrate`](#migrate) first.
If dependency installation fails, the CLI restores the touched source files,
package manifests, and lockfiles.
## diff [#diff]
Show source differences without writing files or installing dependencies.
Pass component names or omit them to review the components recorded in `uitheme.json`.
```sh
npx uitheme-cli diff theme-toggle
npx uitheme-cli diff
npx uitheme-cli diff --json
```
This command stays read-only with `--yes` or `--overwrite`.
JSON output contains the installation plan rather than the text diff.
## migrate [#migrate]
Preview supported legacy imports and dependency declarations before moving to v2.
Migration previews changes by default. Add `--yes` to apply a safe plan.
```sh
npx uitheme-cli migrate
npx uitheme-cli migrate --cwd apps/web --json
npx uitheme-cli migrate --yes
```
Custom control props and unknown provider contexts require manual changes.
When migration reports these cases, it does not write files. Follow the
[migration guide](/docs/migration) before replacing legacy controls.
Migration does not run the package manager. Update the lockfile after applying
its dependency changes.
Migration has its own options. Project and registry options from other commands
do not apply.
| Option | Behavior |
| ------------------------- | -------------------------------------------------------------------------------- |
| `--cwd `, `-c ` | Select the app to migrate. Defaults to the current directory. |
| `--dry-run`, `--preview` | Show the migration plan without applying it. This overrides `--yes`. |
| `--yes`, `-y` | Apply a safe migration plan. Without this flag, migration stays in preview mode. |
| `--json` | Print a structured migration report. This does not authorize writes. |
| `--help`, `-h` | Show migration help. |
## Project options [#project-options]
These options apply to `info`, `init`, `list`, `add`, and `diff`.
Values can use `--option value` or `--option=value`.
Explicit flags override saved `uitheme.json` choices. Saved choices override
compatible component configuration, then automatic detection.
| Option | Values and behavior |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `--cwd ` | Select an existing app. Defaults to the current directory. The `-c` alias is available only for `migrate`. |
| `--framework ` | `vite-react`, `vite-vue`, `next`, `tanstack`, or `nuxt`. Select a supported framework installed in the app. |
| `--base ` | `base-ui` or `radix` for React; `reka` for Vue. |
| `--language ` | `ts` or `js`. Select the language of the installed component source. |
| `--tailwind ` | `3` or `4`. Match the Tailwind major already installed in the app. |
| `--package-manager ` | `npm`, `pnpm`, `yarn`, or `bun`. Select the package manager for dependency installation. |
| `--css `, `--css-file ` | Select the global theme stylesheet. Both names set the same option. |
| `--tailwind-config ` | Select the Tailwind configuration file, such as the config used by Tailwind 3.4. |
| `--components-dir ` | Select where the CLI writes component source. Shared destinations must stay inside the detected workspace and have an owning package manifest. |
| `--alias-prefix ` | Set the import prefix used for installed controls. |
| `--color-format ` | `css` for complete CSS colors; `hsl-channels` for HSL channel tokens. |
## Registry and output options [#registry-and-output-options]
| Option | Behavior |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `--revision ` | Select an immutable registry revision for `list`, `add`, or `diff`. Omit it to use the registry's current stable revision. |
| `--registry ` | Use another registry, for example a local build. The URL must use `https`, or `http` for `localhost` and `127.0.0.1`. |
| `--dry-run`, `--preview` | Preview `init` or `add` without writing files or installing packages. `diff` is always read-only. |
| `--yes`, `-y` | Apply an `init` or `add` plan without questions. Files that differ from the uitheme version are kept. |
| `--overwrite` | Replace files that differ from the uitheme version, without a question. |
| `--json` | Print structured output for `list` or a plan for `init`, `add`, and `diff`. `info` always prints JSON. This flag does not enable preview mode. |
| `--help`, `-h` | Show help without running the selected command. |
Combine JSON output with a preview when you need a plan without writes:
```sh
npx uitheme-cli add theme-toggle --json --dry-run
```
## Environment variables [#environment-variables]
| Variable | Behavior |
| ---------------------- | --------------------------------------------------------------------------------------------------------------- |
| `UITHEME_REGISTRY_URL` | Use another registry for every command. `--registry` wins when you set both. |
| `UITHEME_WEB_VERSION` | Install `uitheme-web` from a dist-tag, a version, or a packed `.tgz` file. The default is `uitheme-web@latest`. |
The CLI prints the registry URL and the runtime when you change them.
```sh
UITHEME_WEB_VERSION=next npx uitheme-cli init
UITHEME_REGISTRY_URL=http://127.0.0.1:4000/r npx uitheme-cli add theme-toggle
```
The CLI always installs the `UITHEME_WEB_VERSION` value, even when the app
already has `uitheme-web`. A `.tgz` path records a `file:` path in
`package.json`. Use it only for local tests, and keep the variable set for
later commands in that app.
## Help and version [#help-and-version]
```sh
npx uitheme-cli --help
npx uitheme-cli add --help
npx uitheme-cli migrate --help
npx uitheme-cli --version
```
`-h` is an alias for `--help`. `-v` is an alias for the top-level `--version`.
Version flags are top-level commands, not options for `add` or other commands.
## Exit codes [#exit-codes]
| Code | Meaning |
| ---- | ----------------------------------------------------------------------------------------------------------------- |
| `0` | The command completed, or a preview completed without errors. |
| `1` | The command failed. Read the error before retrying. |
| `2` | Manual steps remain. Setup may have applied safe changes. Migration with manual diagnostics does not write files. |
# Migrate to v2
## Web controls [#web-controls]
Upgrade the runtime to `uitheme-web@latest`. Keep providers, hooks, theme values,
storage keys, and callbacks. Test the saved appearance and palette before and
after the upgrade.
Styled controls are no longer exported by the v2 runtime. Install the controls
as editable source with `uitheme-cli`, then change imports to your local files.
The runtime does not depend on Base UI, Radix, or Reka.
```sh
npx uitheme-cli migrate --dry-run
pnpm add uitheme-web@latest
npx uitheme-cli add theme-toggle theme-selector color-theme-selector --base base-ui
npx uitheme-cli migrate --dry-run
```
Use your app’s package manager for the dependency upgrade. Work on a migration
branch. The app can have unresolved imports until you finish replacing the old
controls. `add` requires v2 and does not run on an installed v1 runtime.
For controls without custom props inside a recognized provider, review the migration preview.
Apply the migration with `npx uitheme-cli migrate --yes`. Controls with custom props or unrecognized providers need manual changes.
The command reports these cases before it writes files.
Choose `--base radix` for Radix React controls. Vue uses `--base reka`.
Select the Tailwind major already used by the app with `--tailwind 3` or `--tailwind 4`.
The installed files use the selected base. They do not switch bases at runtime.
Replace the previous packaged control imports with the installed paths:
```tsx
import { ThemeToggle } from '@/components/ui/theme-toggle';
import { ThemeSelector } from '@/components/ui/theme-selector';
```
Check the destinations in `uitheme.json` if your app uses another alias or
component directory. Preserve customized controls until you have reviewed
and tested their replacements. `--yes` does not overwrite edits.
## Vue v1 [#vue-v1]
The published `uitheme-vue@1` package remains available. There is no compatibility
package. To adopt v2, install `uitheme-web@latest` and move Vue runtime imports to
`uitheme-web/vue`. Use `uitheme-web/vue/bootstrap` for server bootstrap.
Replace styled controls with local Reka source installed by `uitheme-cli`.
For Nuxt 4, replace the previous Vue module entry with `uitheme-web/nuxt`.
Nuxt 3 automatic migration is not supported. Review framework compatibility
before changing the Nuxt module.
## Verify the app [#verify-the-app]
Check appearance and palette persistence after reload. Check the initial
server HTML and hydration with light, dark, and system preferences.
Check both transition directions and reduced motion.
Check selector keyboard navigation, translated labels, and the duration value.
Set transition style and duration on each control with `animationType` and `duration` props.
# LLM documentation
Use these routes when a coding agent needs documentation without the website layout.
The text comes from the same MDX files as the visible pages.
## Documentation index [#documentation-index]
Open [`/llms.txt`](/llms.txt) for a list of guides and links to their Markdown versions.
The index includes Getting Started and all component pages for Base UI, Radix, and Reka.
## Full documentation [#full-documentation]
Open [`/llms-full.txt`](/llms-full.txt) for all guides in one text file.
Component guides include installation commands, usage examples, API details, and source files.
These component examples use Tailwind 4 and TypeScript.
## Individual guides [#individual-guides]
Append `.mdx` to a documentation URL to read its Markdown version.
For example, open [the installation guide as Markdown](/docs/installation.mdx).
Component pages also have Markdown versions.
Open [Theme Toggle for Base UI as Markdown](/docs/components/base-ui/theme-toggle.mdx) for an example.
## Instructions for coding agents [#instructions-for-coding-agents]
- Read the installation guide for the app’s framework before changing files.
- Use the runtime import that matches the framework.
- Import styled controls from installed local files.
- Keep appearance, color palette, and transition settings separate.
- Review CLI changes with `--dry-run` before applying them.
- Check behavior in the running app. Documentation alone does not confirm that an integration works.
# React
## Install [#install]
Install the runtime in your existing React app. Keep your current styling system.
```sh
npm install uitheme-web@latest
```
## Add the provider [#add-the-provider]
Wrap the part of your app that needs theme state. Register every palette key that your CSS defines.
```tsx
import { UIThemeProvider } from 'uitheme-web/react';
export function App() {
return (
);
}
```
## Change the theme [#change-the-theme]
Call the hook inside the provider. Pass the clicked element to anchor an animated transition.
```tsx
import { useUITheme } from 'uitheme-web/react';
export function DarkModeButton() {
const { switchTheme } = useUITheme();
return (
);
}
```
Use `setTheme` for an immediate change. Use `switchColorTheme` to change a registered palette. `theme` stores the preference; `resolvedTheme` is light or dark.
## Add styles and controls [#add-styles-and-controls]
Define your light, `.dark`, and `.theme-caffeine` styles. Follow [Tailwind and palette setup](/docs/installation#tailwind-and-palettes) when using registry controls.
For editable controls, preview the installation first:
```sh
npx uitheme-cli init --dry-run
npx uitheme-cli init
npx uitheme-cli add theme-toggle theme-selector color-theme-selector
```
For Vite, place the provider around your root app in `src/main.tsx`. For server rendering, use the [Next.js](/docs/guides/nextjs) or [TanStack Start](/docs/guides/tanstack-start) guide.
## Example [#example]
See the [React controls in the Next.js example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-next/components/theme). These controls use the same React hook. The complete app uses the Next.js provider described in its guide.
# Next.js
## Install [#install]
```sh
npm install uitheme-web@latest
```
## Create a client provider [#create-a-client-provider]
```tsx title="app/providers.tsx"
'use client';
import { NextUIThemeProvider } from 'uitheme-web/next';
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Wrap your root layout [#wrap-your-root-layout]
Keep the layout as a Server Component. The provider includes the script that applies the saved appearance before hydration.
```tsx title="app/layout.tsx"
import './globals.css';
import { Providers } from './providers';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
{children}
);
}
```
Keep theme hooks and interactive controls in Client Components. Use `uitheme-web/next/server` when you need server-safe bootstrap helpers. Do not import browser hooks into server code.
## Add a control [#add-a-control]
Use the [React hook example](/docs/guides/react#change-the-theme), or install a [Theme Toggle](/docs/components/base-ui/theme-toggle). Set up [Tailwind and palette styles](/docs/installation#tailwind-and-palettes) before installing styled controls.
Check the first page load, saved preferences after reload, and system appearance changes. If your site uses a Content Security Policy, configure its nonce through the provider's supported options.
## Example [#example]
Open the [complete Next.js example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-next). Start with `app/providers.tsx` and `app/layout.tsx`.
# TanStack Start
## Install [#install]
Use an existing TanStack Start application.
```sh
npm install uitheme-web@latest
```
## Apply the appearance before hydration [#apply-the-appearance-before-hydration]
Put the theme script in your root document's head. Keep your existing `HeadContent`, `Scripts`, and route setup.
```tsx
import { TanStackStartThemeScript } from 'uitheme-web/react';
;
```
Add `suppressHydrationWarning` to the root `html` element. Wrap your route content in the matching provider:
```tsx
import { TanStackUIThemeProvider } from 'uitheme-web/react';
;
```
Use the same theme options and storage keys for the script and provider. Add your appearance and palette CSS, then use the [React controls](/docs/guides/react#add-styles-and-controls).
## Render from a saved cookie [#render-from-a-saved-cookie]
Choose cookie-based rendering when the server must know the saved preference. The helpers in `uitheme-web/tanstack` build the initial theme data, HTML attributes, and bootstrap script.
Create theme data separately for each request. Read and write cookies through your server functions. Pass the returned initial values to the provider and connect preference-change callbacks to your persistence code.
Start from the working server example below rather than sharing mutable theme state between requests. Check two requests with different cookies and reload each appearance.
## Examples [#examples]
- [Client bootstrap example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-tanstack): root document script and client provider.
- [Cookie-based server example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-tanstack-ssr): request-specific theme state and cookie persistence.
# Vue
## Install [#install]
```sh
npm install uitheme-web@latest
```
## Register the plugin [#register-the-plugin]
```ts title="src/main.ts"
import { createApp } from 'vue';
import { createUITheme } from 'uitheme-web/vue';
import App from './App.vue';
createApp(App)
.use(
createUITheme({
defaultTheme: 'system',
colorThemes: ['default', 'caffeine'],
})
)
.mount('#app');
```
Import your stylesheet from the app entry. Define styles for the registered palette keys and the light and dark appearances.
## Use the composable [#use-the-composable]
```vue
```
Call `useUITheme` during setup. Its state values are refs. Pass the click event to use the button as the transition origin. Use `setTheme` for an immediate change.
For a separate scope, wrap a subtree in `UIThemeProvider`. For server rendering, create a fresh app and plugin per request. Use the [Nuxt guide](/docs/guides/nuxt) for automatic Nuxt integration.
## Add editable controls [#add-editable-controls]
The Vue registry uses Reka. Set up [Tailwind and palette styles](/docs/installation#tailwind-and-palettes), then preview the CLI changes.
```sh
npx uitheme-cli init --dry-run
npx uitheme-cli init
npx uitheme-cli add theme-toggle theme-selector color-theme-selector --base reka
```
## Example [#example]
Open the [Vue application](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-vue). Its shared screens and styles live in the [Vue demo workspace](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/vue-theme-demo).
# Nuxt
## Install [#install]
The module supports Nuxt 4. The CLI rejects Nuxt 3 automatic setup before changing files.
```sh
npm install uitheme-web@latest
```
## Register the module [#register-the-module]
```ts title="nuxt.config.ts"
export default defineNuxtConfig({
modules: ['uitheme-web/nuxt'],
uiTheme: {
defaultTheme: 'system',
colorThemes: ['default', 'caffeine'],
},
});
```
The module supplies the theme integration. Keep palette styles in your application CSS. Do not add a second app-wide plugin or provider around the module's setup.
## Use theme state [#use-theme-state]
Use `useUITheme` from `uitheme-web/vue` in a component's setup. The [Vue composable example](/docs/guides/vue#use-the-composable) also applies to Nuxt.
Use a fresh state for each server request. Check the initial appearance before hydration and the saved preference after reload. Keep the registered palette keys consistent with your CSS.
## Install controls [#install-controls]
With Tailwind configured, preview the changes before applying them:
```sh
npx uitheme-cli init --dry-run
npx uitheme-cli init
npx uitheme-cli add theme-toggle theme-selector color-theme-selector --base reka
```
Keep controls under the app's configured component alias. Nuxt 4 normally places application code under `app/`.
## Example [#example]
Open the [Nuxt example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-nuxt). Review `nuxt.config.ts` and the [shared Vue screens](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/vue-theme-demo).
# Expo and React Native
## Install [#install]
Use `uitheme-rn` for native apps. The web runtime and web registry controls are separate.
```sh
npx expo install uitheme-rn
```
## Add the provider [#add-the-provider]
Wrap the root layout or application in `UIThemeProvider`. With Expo Router, keep the router inside the provider.
```tsx title="src/app/_layout.tsx"
import { Stack } from 'expo-router';
import { UIThemeProvider } from 'uitheme-rn';
export default function RootLayout() {
return (
);
}
```
## Use native colors [#use-native-colors]
```tsx
import { Text, View } from 'react-native';
import { useUITheme } from 'uitheme-rn';
export function Screen() {
const { colors } = useUITheme();
return (
Appearance
);
}
```
Use `StyleSheet` or normal native style props. Pass custom palettes through the provider when needed. Configure a storage adapter to persist preferences, and wait for `isHydrated` before hiding a launch splash screen when your app uses saved theme state.
For snapshot-based screen transitions, follow the example's transition provider and native dependency setup. Keep native navigation transitions coordinated with theme transitions.
## Styling adapters [#styling-adapters]
Use the [NativeWind guide](/docs/guides/nativewind) or [Uniwind guide](/docs/guides/uniwind) when your app uses utility classes. The native examples own their controls; the web registry does not install native components.
## Example [#example]
Open the [Expo example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-expo). Review `src/app/_layout.tsx`, `src/storage.ts`, and `src/theme/` for navigation, persistence, and custom palettes.
# NativeWind
## Start with the native provider [#start-with-the-native-provider]
Complete the [Expo provider setup](/docs/guides/expo). Install and configure the NativeWind version supported by your app and Expo SDK. This adapter uses the NativeWind 5 release candidate with Tailwind 4. Use the compatible versions and Metro configuration from the example below.
## Keep the CSS compiler compatible [#keep-the-css-compiler-compatible]
With NativeWind `5.0.0-rc.0` and Expo 58, pin Metro's Lightning CSS dependency to
`1.30.1`. The native compiler loads this copy. A newer copy can fail with a
`failed to deserialize` error.
For pnpm, add this override to the workspace root `pnpm-workspace.yaml`:
```yaml
overrides:
'@expo/metro-config@58>lightningcss': 1.30.1
```
Keep the web Tailwind compiler on its own supported version. See the repository's
workspace configuration and NativeWind example for the tested setup.
## Map your theme scopes [#map-your-theme-scopes]
Create a scope name for each light and dark palette in your stylesheet. Use the same names in the adapter map:
```tsx
import { UIThemeProvider } from 'uitheme-rn';
import { NativeWindTheme, type ThemeStyleScopes } from 'uitheme-rn/nativewind';
const scopes: ThemeStyleScopes = {
default: { light: 'default-light', dark: 'default-dark' },
};
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Import your global CSS through the app entry. Start from the example's CSS and theme map below. Keep semantic colors in theme variables rather than adding separate light and dark colors to every control.
## Use one source of state [#use-one-source-of-state]
Read preferences and change themes with `useUITheme` from `uitheme-rn`. Let `NativeWindTheme` synchronize the class-based appearance. Add custom palette keys to both the provider and scope map.
Check the selected appearance, registered palette, saved preference after reload, and reduced motion. Keep your Expo-compatible versions of React Native and Reanimated.
## Example [#example]
Open the [NativeWind example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-nativewind). Copy the relevant setup from `src/global.css`, `src/theme.ts`, and `src/app/_layout.tsx`.
# Uniwind
## Start with the native provider [#start-with-the-native-provider]
Complete the [Expo provider setup](/docs/guides/expo). Install Uniwind and Tailwind 4 using the versions supported by your Expo SDK.
## Configure Metro and CSS [#configure-metro-and-css]
Register your global CSS entry with `withUniwindConfig` from `uniwind/metro`. Include the named theme scopes in `extraThemes`. Import the stylesheet from the app entry.
Use the same scope names in your CSS, Metro configuration, and UITheme adapter map:
```tsx
import { UIThemeProvider } from 'uitheme-rn';
import { UniwindTheme, type ThemeStyleScopes } from 'uitheme-rn/uniwind';
const scopes: ThemeStyleScopes = {
default: { light: 'default-light', dark: 'default-dark' },
};
export function Providers({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Use the adapter [#use-the-adapter]
Read preferences and change themes with `useUITheme` from `uitheme-rn`. The adapter keeps Uniwind synchronized with the selected appearance and palette. Register custom palette keys with the provider and add their scopes to the map.
Use semantic utility classes for your controls. Keep their colors, radii, and other supported values in the active theme. Check both appearances, palette changes, persistence after reload, and reduced motion.
## Example [#example]
Open the [Uniwind example](https://github.com/tonyedgal/ui-theme/tree/main/apps/examples/example-uniwind). Review `metro.config.cjs`, `src/global.css`, `src/theme.ts`, and `src/app/_layout.tsx` together.
# Components
# 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 (
);
}
```
### 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 (
);
}
```
### 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 (
);
}
```
## 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 (
);
}
```
## 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;
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 (
);
});
```
## 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.
# Icon Theme Toggle · Base UI
Animate between sun and moon icons while changing the UI theme.
## 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 { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
### 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 { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
### Slide icons
Sun and moon exchange places with a short sideways movement.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
### Blur icons
A brief blur and crossfade joins the two states.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
## When to use
- Use a compact light/dark action in a header or toolbar.
- Use a short icon animation to make the changed state clear.
## When not to use
- People need to choose the system preference. Use Theme Switcher or Theme Selector.
- The control changes a palette. Use Color Theme Selector.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app. Then install the component.
The CLI copies Icon Theme Toggle and its Theme Toggle dependency. Both reuse the shared `cn` utility.
```sh
npx uitheme-cli add icon-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/icon-theme-toggle
```
## Usage
Use the control inside your theme provider. Import it from your app’s component directory.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
## API reference
These props belong to the installed control. Its page transition settings override the provider for this action only.
| 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. |
| effect | 'rotate' | 'slide' | 'blur' | Icon motion only. Defaults to rotate and is separate from the page transition. |
## Keyboard
| Key | Action |
| ------------- | ---------------------------------------------------------------------------------------------- |
| Enter / Space | Toggle the resolved light/dark theme. Keyboard activation changes state without icon movement. |
## Accessibility
- Keep the accessible label when changing the visible content. Icons are decorative.
- The control stays disabled until the theme provider mounts.
- Reduced motion removes travel and rotation. Icon state keeps a short opacity change. The selection surface changes position without travel.
- Keyboard activation changes the theme without the pointer animation.
## Theme behavior
The sun represents resolved light mode. The moon represents resolved dark mode. Activation replaces a system preference with the opposite resolved theme. The clicked button sets the page transition origin.
## AI Agent notes
- Use `uitheme-web/react` for React or `uitheme-web/vue` for Vue. Keep the theme provider around the control.
- Keep page transitions in the control’s `animationType` and `duration` props. Do not add a standalone transition-settings control.
- Keep input labels, focus styles, selected state, disabled state, and reduced-motion behavior when editing source.
- Use the shared theme tokens. Do not hardcode a palette or radius.
## 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 control installs one main component file.
### icon-theme-toggle.tsx
```
'use client';
import * as React from 'react';
import { SunIcon, MoonIcon } from 'lucide-react';
import { useUITheme } from 'uitheme-web/react';
import { ThemeToggle, type ThemeToggleProps } from './theme-toggle';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
export type IconThemeToggleProps = Omit<
ThemeToggleProps,
'display' | 'children' | 'animationType' | 'duration' | 'slideDirection'
> &
ThemeControlTransitionProps & {
effect?: 'rotate' | 'slide' | 'blur';
};
export const IconThemeToggle = React.forwardRef<
HTMLButtonElement,
IconThemeToggleProps
>(function IconThemeToggle(
{ effect = 'rotate', label, classes, onKeyDown, onPointerDown, ...props },
ref
) {
const { resolvedTheme } = useUITheme();
const [keyboard, setKeyboard] = React.useState(false);
const dark = resolvedTheme === 'dark';
const transition = keyboard
? 'transition-none motion-reduce:transition-none'
: 'transition-[transform,translate,rotate,scale,opacity,filter] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-opacity motion-reduce:duration-100';
const hidden = (side: 'sun' | 'moon') =>
effect === 'slide'
? side === 'sun'
? '-translate-x-2'
: 'translate-x-2'
: effect === 'blur'
? 'scale-75 blur-[4px]'
: side === 'sun'
? '-rotate-90 scale-75'
: 'rotate-90 scale-75';
const layer =
'col-start-1 row-start-1 inline-flex size-4 items-center justify-center motion-reduce:!transform-none motion-reduce:!translate-x-0 motion-reduce:!rotate-0 motion-reduce:!scale-100 motion-reduce:!blur-none';
return (
{
setKeyboard(true);
onKeyDown?.(event);
}}
onPointerDown={(event) => {
setKeyboard(false);
onPointerDown?.(event);
}}
classes={{
...classes,
trigger: cn(
'w-10 px-0 active:scale-[0.96] motion-reduce:active:scale-100',
classes?.trigger
),
}}
>
);
});
```
### 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;
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 (
);
});
```
## 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 `effect="rotate"` (default), `effect="slide"`, or `effect="blur"` for the icon exchange. Icon motion lasts 200ms and retargets from its current state on another pointer activation. Use `animationType`, `duration`, and `slideDirection` for the separate UI theme transition. Use native button props, `label`, and `classes` to customize the control.
# Theme Switcher · Base UI
Choose system, light, or dark with a moving selection surface.
## 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 { ThemeSwitcher } from '@/components/ui/theme-switcher';
export function Example() {
return (
);
}
```
### 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 { ThemeSwitcher } from '@/components/ui/theme-switcher';
export function Example() {
return (
);
}
```
## When to use
- Show all three appearance choices in a header or settings row.
- Use a moving selection surface to connect the old and new choice.
## When not to use
- Space permits only one compact action. Use Icon Theme Toggle.
- People need a longer list of named choices. Use Theme Selector.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app. Then install the component.
The CLI copies one component file and reuses the shared `cn` utility.
```sh
npx uitheme-cli add theme-switcher --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-switcher
```
## Usage
Use the control inside your theme provider. Import it from your app’s component directory.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeSwitcher } from '@/components/ui/theme-switcher';
export function Example() {
return (
);
}
```
## API reference
These props belong to the installed control. Its page transition settings override the provider for this action only.
| 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 root, item, and selection indicator 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. |
## Keyboard
| Key | Action |
| ---------- | -------------------------------------- |
| Tab | Enter or leave the radio group. |
| Arrow keys | Move and select among enabled choices. |
| Space | Select the focused radio. |
## Accessibility
- Keep the accessible label when changing the visible content. Icons are decorative.
- The control stays disabled until the theme provider mounts.
- Reduced motion removes travel and rotation. Icon state keeps a short opacity change. The selection surface changes position without travel.
- Keyboard activation changes the theme without the pointer animation.
## Theme behavior
The checked radio follows the stored theme preference, including system. The selected surface moves between the three equal tracks. The icons brighten on fine-pointer hover. The group disables choices that the provider does not support.
## AI Agent notes
- Use `uitheme-web/react` for React or `uitheme-web/vue` for Vue. Keep the theme provider around the control.
- Keep page transitions in the control’s `animationType` and `duration` props. Do not add a standalone transition-settings control.
- Keep input labels, focus styles, selected state, disabled state, and reduced-motion behavior when editing source.
- Use the shared theme tokens. Do not hardcode a palette or radius.
## 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 control installs one main component file.
### theme-switcher.tsx
```
'use client';
import * as React from 'react';
import { useUITheme, type Theme } from 'uitheme-web/react';
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
export type ThemeSwitcherProps = ThemeControlTransitionProps & {
label?: string;
disabled?: boolean;
borderStyle?: 'solid' | 'shadow';
className?: string;
classes?: {
root?: string;
item?: string;
indicator?: string;
};
};
const choices: readonly Theme[] = ['system', 'light', 'dark'];
export function ThemeSwitcher({
label = 'Theme',
disabled = false,
animationType,
duration,
slideDirection,
borderStyle = 'solid',
className,
classes,
}: ThemeSwitcherProps) {
const { theme, themes, mounted, switchTheme } = useUITheme();
const id = React.useId();
const [keyboard, setKeyboard] = React.useState(false);
return (
);
}
```
## 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.
# Theme Selector · Base UI
Choose light, dark, or the system appearance.
## 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 { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
### 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 { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
### Icon with text
Show an icon beside the theme label.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
## When to use
- Let people choose light, dark, or system appearance in app settings.
- Show the appearance options configured on the provider in one labeled control.
- Use the floating menu in headers, toolbars, and compact layouts.
## When not to use
- A toolbar needs only a quick light/dark action. Use Theme Toggle.
- The choice changes brand colors rather than appearance. 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-selector --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-selector
```
## 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 { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
For inline choices, install [Theme Disclosure](/docs/components/base-ui/theme-disclosure).
The floating selector does not install disclosure code.
## 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 root, trigger, content, item, and label 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` | Open the selector or confirm the focused option. |
| `Arrow keys` | Move between available appearance options. |
| `Escape` | Close the menu and return focus to the trigger. |
## Accessibility
- Keep the label connected to the trigger. Preserve the primitive’s focus behavior.
- The option list comes from the provider. Avoid showing a system option when that option is not configured.
## Theme behavior
- Use `theme` for the selected preference. `System` stays selected when the operating system changes appearance.
- Changes use `switchTheme`. The trigger sets the transition origin. The color palette does not change.
## 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 the provider’s `themes` array. Show only the options configured on the provider.
- Use `theme` for the selected value. Use `resolvedTheme` for visual appearance.
- 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-selector.tsx
```
'use client';
import * as React from 'react';
import { useUITheme } from 'uitheme-web/react';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
import { Select } from '@base-ui/react/select';
export type ThemeSelectorClasses = {
root?: string;
trigger?: string;
content?: string;
item?: string;
label?: string;
};
export type ThemeSelectorProps = ThemeControlTransitionProps & {
display?: 'text' | 'icon-text';
borderStyle?: 'solid' | 'shadow';
className?: string;
classes?: ThemeSelectorClasses;
label?: string;
disabled?: boolean;
};
export const ThemeSelector = React.forwardRef<
HTMLButtonElement,
ThemeSelectorProps
>(function ThemeSelector(
{
animationType,
duration,
slideDirection,
borderStyle = 'solid',
display = 'text',
className,
classes,
label = 'Theme',
disabled = false,
}: ThemeSelectorProps,
forwardedRef
) {
const { theme, themes, mounted, switchTheme } = useUITheme();
const triggerRef = React.useRef(null);
const selected = theme;
const options: {
value: string;
label: string;
}[] = themes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
React.useImperativeHandle(forwardedRef, () => triggerRef.current!);
const change = (
value: string,
element: HTMLElement | null = triggerRef.current
) => {
const next = themes.find((theme) => theme === value);
if (next)
void switchTheme(next, {
element,
animationType,
duration,
slideDirection,
});
};
return (
);
});
```
## 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.
# Color Theme Selector · Base UI
Choose a color palette without changing the appearance.
## 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 { ColorThemeSelector } from '@/components/ui/color-theme-selector';
export function Example() {
return (
);
}
```
### 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 { ColorThemeSelector } from '@/components/ui/color-theme-selector';
export function Example() {
return (
);
}
```
## When to use
- Let people choose an app palette without changing light or dark appearance.
- Show readable labels for the palette keys configured on the provider.
- Use the floating menu in headers, toolbars, and compact layouts.
## When not to use
- People are choosing light, dark, or system. Use Theme Selector.
- You need a free-form color picker. This control selects registered palette keys.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install the component.
```sh
npx uitheme-cli add color-theme-selector --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/color-theme-selector
```
## 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 { ColorThemeSelector } from '@/components/ui/color-theme-selector';
export function Example() {
return (
);
}
```
For inline choices, install [Color Theme Disclosure](/docs/components/base-ui/color-theme-disclosure).
The floating selector does not install disclosure code.
## 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 root, trigger, content, item, and label 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. |
| options | { value, label }[] | Change palette labels. Values must already exist in the provider’s colorThemes. |
## Keyboard
| Key | Action |
| --------------- | ------------------------------------------------- |
| `Enter / Space` | Open the selector or confirm the focused palette. |
| `Arrow keys` | Move between palette options. |
| `Escape` | Close the menu and return focus to the trigger. |
## Accessibility
- Use readable option labels instead of relying on color swatches alone.
- Use option values from `colorThemes`. Custom labels do not register new palettes.
## Theme behavior
- The control calls `switchColorTheme`. The appearance preference does not change.
- Register each palette key on the provider. Define its light and dark CSS variables. Selecting a key does not create its colors.
## 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`.
- Keep appearance and `colorTheme` separate. Use the palette action to change colors.
- Check that the selected palette changes the computed semantic colors in light and dark appearance. A changed label alone does not confirm this.
- 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.
### color-theme-selector.tsx
```
'use client';
import * as React from 'react';
import { useUITheme } from 'uitheme-web/react';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
import { Select } from '@base-ui/react/select';
export type ColorThemeOption = {
value: string;
label: string;
};
export type ColorThemeSelectorClasses = {
root?: string;
trigger?: string;
content?: string;
item?: string;
label?: string;
};
export type ColorThemeSelectorProps = ThemeControlTransitionProps & {
borderStyle?: 'solid' | 'shadow';
className?: string;
classes?: ColorThemeSelectorClasses;
label?: string;
options?: ColorThemeOption[];
disabled?: boolean;
};
export const ColorThemeSelector = React.forwardRef<
HTMLButtonElement,
ColorThemeSelectorProps
>(function ColorThemeSelector(
{
animationType,
duration,
slideDirection,
borderStyle = 'solid',
className,
classes,
label = 'Color theme',
options: optionOverrides,
disabled = false,
}: ColorThemeSelectorProps,
forwardedRef
) {
const { colorTheme, colorThemes, mounted, switchColorTheme } = useUITheme();
const triggerRef = React.useRef(null);
const selected = colorTheme;
const options: {
value: string;
label: string;
}[] =
optionOverrides ??
colorThemes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
React.useImperativeHandle(forwardedRef, () => triggerRef.current!);
const change = (
value: string,
element: HTMLElement | null = triggerRef.current
) =>
switchColorTheme(value, {
element,
animationType,
duration,
slideDirection,
});
return (
);
});
```
## 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.
Pass `animationType`, `duration`, and `slideDirection` to set this control's palette 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.
# Theme Disclosure · Base UI
Expand appearance choices within the page layout.
## 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 { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
### Opens upward
Place the choices above the trigger and the label below it. Reserved space keeps the preview and trigger in place.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
### Icon with text
Show an icon beside the theme label.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
## When to use
- Show appearance choices in a settings panel with space in the expansion direction.
- Keep the choices open while people compare their selection.
- Use a bounded scroll area for a longer list.
## When not to use
- Expansion would cover or move content in a compact toolbar. Use [Theme Selector](/docs/components/base-ui/theme-selector).
- You need a general content disclosure. This control selects a theme preference.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install this control. The floating selector is a separate component.
```sh
npx uitheme-cli add theme-disclosure --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-disclosure
```
## Usage
Place the control inside your theme provider. Expansion moves the content below
it. The panel stays open after a selection. Its scroll area defaults to 240px and
never exceeds 60% of the viewport height.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
Set `direction="up"` to place the choices above the trigger. Leave space above
for the expanded panel. Anchor the control at the bottom of its container if the
trigger must stay in place. The Examples section shows this layout. The default
is `direction="down"`.
Keep the label below the trigger when the panel opens upward. The component
places the label there automatically. Keep the label above a downward-opening
trigger. Preserve this order when you customize the layout.
Reserve enough height for the expanded panel, trigger, label, and frame padding.
Align downward controls to the top and upward controls to the bottom. The
examples reserve `h-96` with the default `maxHeight={240}`. Adjust the space if
you change the scroll limit. Opening a panel must not shift its trigger or the
surrounding preview.
## API reference
Use `open` with the change callback to control expansion. Use `defaultOpen` for
an initial expanded state. Use `maxHeight` to set the scroll limit.
| Prop | Type | Behavior |
| --- | --- | --- |
| label | string | Override the readable accessible label. |
| className | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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. |
| open | boolean | Control whether the inline panel is expanded. |
| defaultOpen | boolean | Start with the panel expanded. Defaults to false. |
| onOpenChange | (open: boolean) => void | Receive the next expanded state when the trigger is pressed. |
| direction | 'down' | 'up' | Place choices below or above the trigger. Defaults to down. Use a bottom-anchored layout to keep an upward trigger in place. |
| maxHeight | number | Limit the scroll area in pixels. Defaults to 240 and never exceeds 60% of the viewport height. |
## Keyboard
| Key | Action |
| --------------- | -------------------------------------------------------------------- |
| `Enter / Space` | Expand or collapse the focused trigger. |
| `Tab` | Move from the trigger into the expanded panel and its radio choices. |
| `Arrow keys` | Change the selected radio within the group. |
| `Space` | Select the focused radio. |
The panel stays open until you collapse it. Escape does not close an inline panel.
## Accessibility
- Keep the visible label, trigger name, and radio group name.
- The trigger reports the expanded state. Hidden choices leave the tab order.
- The disabled state prevents expansion and selection.
- Keyboard controls use native buttons and radio inputs.
## Theme behavior
The checked radio follows `theme`, including `system`. Choices come from `themes`. The color palette does not change.
The selected radio sets the transition origin. The library performs the page
transition. Omitted transition props keep the provider settings.
## AI Agent notes
- Import the local control. Use `uitheme-web/react` or `uitheme-web/vue` for the runtime.
- Read the configured provider choices. Do not invent theme values.
- Preserve the label, focus styles, selected state, disabled state, and scroll limit.
- Keep the label below an upward trigger and above a downward trigger.
- Reserve room for the full bounded panel, trigger, label, and frame padding.
- Anchor upward controls at the bottom and downward controls at the top. Opening the panel must not shift the preview.
- Use the separate floating selector in compact layouts.
- Keep page transitions in the library. Do not add a local transition helper.
- Check keyboard selection, preference persistence, and reduced motion in the running app.
## Source
The CLI reuses the shared `cn` utility. It repairs a missing utility and checks an
existing utility before offering a replacement. This control installs one main
component file. It does not install the floating selector.
### theme-disclosure.tsx
```
'use client';
import * as React from 'react';
import {
useUITheme,
type ThemeControlTransitionProps,
} from 'uitheme-web/react';
import { ChevronDownIcon, SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
export type ThemeDisclosureProps = ThemeControlTransitionProps & {
label?: string;
disabled?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
maxHeight?: number;
direction?: 'up' | 'down';
display?: 'text' | 'icon-text';
className?: string;
classes?: Partial<
Record<'root' | 'trigger' | 'content' | 'item' | 'label', string>
>;
};
export const ThemeDisclosure = React.forwardRef<
HTMLButtonElement,
ThemeDisclosureProps
>(function ThemeDisclosure(
{
label = 'Theme',
disabled = false,
open,
defaultOpen = false,
onOpenChange,
maxHeight = 240,
direction = 'down',
display = 'text',
animationType,
duration,
slideDirection,
className,
classes,
},
ref
) {
const { theme, themes, mounted, switchTheme } = useUITheme();
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const expanded = open ?? internalOpen;
const id = React.useId();
const options = themes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
const selected = options.find((option) => option.value === theme);
const style: React.CSSProperties & { '--disclosure-max-height': string } = {
'--disclosure-max-height': `${maxHeight}px`,
};
return (
{label}
);
});
```
## Customization
Use the `classes` prop to style the root, label, trigger, content, and items.
The frame uses theme tokens. Its outer radius equals the trigger radius plus
the frame padding. This control uses solid borders.
Set `animationType` and `duration` for the page transition. Set `slideDirection`
only with `animationType="slide"`. Keep enough space in the expansion direction.
# Color Theme Disclosure · Base UI
Expand color palette choices within the page layout.
## 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 { ColorThemeDisclosure } from '@/components/ui/color-theme-disclosure';
export function Example() {
return (
);
}
```
### Opens upward
Place the choices above the trigger and the label below it. Reserved space keeps the preview and trigger in place.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ColorThemeDisclosure } from '@/components/ui/color-theme-disclosure';
export function Example() {
return (
);
}
```
## When to use
- Show color palette choices in a settings panel with space in the expansion direction.
- Keep the choices open while people compare their selection.
- Use a bounded scroll area for a longer list.
## When not to use
- Expansion would cover or move content in a compact toolbar. Use [Color Theme Selector](/docs/components/base-ui/color-theme-selector).
- You need a general content disclosure. This control selects a theme preference.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install this control. The floating selector is a separate component.
```sh
npx uitheme-cli add color-theme-disclosure --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/color-theme-disclosure
```
## Usage
Place the control inside your theme provider. Expansion moves the content below
it. The panel stays open after a selection. Its scroll area defaults to 240px and
never exceeds 60% of the viewport height.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ColorThemeDisclosure } from '@/components/ui/color-theme-disclosure';
export function Example() {
return (
);
}
```
Set `direction="up"` to place the choices above the trigger. Leave space above
for the expanded panel. Anchor the control at the bottom of its container if the
trigger must stay in place. The Examples section shows this layout. The default
is `direction="down"`.
Keep the label below the trigger when the panel opens upward. The component
places the label there automatically. Keep the label above a downward-opening
trigger. Preserve this order when you customize the layout.
Reserve enough height for the expanded panel, trigger, label, and frame padding.
Align downward controls to the top and upward controls to the bottom. The
examples reserve `h-96` with the default `maxHeight={240}`. Adjust the space if
you change the scroll limit. Opening a panel must not shift its trigger or the
surrounding preview.
## API reference
Use `open` with the change callback to control expansion. Use `defaultOpen` for
an initial expanded state. Use `maxHeight` to set the scroll limit.
| Prop | Type | Behavior |
| --- | --- | --- |
| label | string | Override the readable accessible label. |
| className | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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. |
| options | { value, label }[] | Change palette labels. Values must already exist in the provider’s colorThemes. |
| open | boolean | Control whether the inline panel is expanded. |
| defaultOpen | boolean | Start with the panel expanded. Defaults to false. |
| onOpenChange | (open: boolean) => void | Receive the next expanded state when the trigger is pressed. |
| direction | 'down' | 'up' | Place choices below or above the trigger. Defaults to down. Use a bottom-anchored layout to keep an upward trigger in place. |
| maxHeight | number | Limit the scroll area in pixels. Defaults to 240 and never exceeds 60% of the viewport height. |
## Keyboard
| Key | Action |
| --------------- | -------------------------------------------------------------------- |
| `Enter / Space` | Expand or collapse the focused trigger. |
| `Tab` | Move from the trigger into the expanded panel and its radio choices. |
| `Arrow keys` | Change the selected radio within the group. |
| `Space` | Select the focused radio. |
The panel stays open until you collapse it. Escape does not close an inline panel.
## Accessibility
- Keep the visible label, trigger name, and radio group name.
- The trigger reports the expanded state. Hidden choices leave the tab order.
- The disabled state prevents expansion and selection.
- Keyboard controls use native buttons and radio inputs.
## Theme behavior
The checked radio follows `colorTheme`. Choices come from `colorThemes`. Appearance does not change.
The selected radio sets the transition origin. The library performs the page
transition. Omitted transition props keep the provider settings.
## AI Agent notes
- Import the local control. Use `uitheme-web/react` or `uitheme-web/vue` for the runtime.
- Read the configured provider choices. Do not invent theme values.
- Preserve the label, focus styles, selected state, disabled state, and scroll limit.
- Keep the label below an upward trigger and above a downward trigger.
- Reserve room for the full bounded panel, trigger, label, and frame padding.
- Anchor upward controls at the bottom and downward controls at the top. Opening the panel must not shift the preview.
- Use the separate floating selector in compact layouts.
- Keep page transitions in the library. Do not add a local transition helper.
- Check keyboard selection, preference persistence, and reduced motion in the running app.
## Source
The CLI reuses the shared `cn` utility. It repairs a missing utility and checks an
existing utility before offering a replacement. This control installs one main
component file. It does not install the floating selector.
### color-theme-disclosure.tsx
```
'use client';
import * as React from 'react';
import {
useUITheme,
type ThemeControlTransitionProps,
} from 'uitheme-web/react';
import { ChevronDownIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
export type ColorThemeDisclosureProps = ThemeControlTransitionProps & {
label?: string;
disabled?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
maxHeight?: number;
direction?: 'up' | 'down';
options?: readonly { value: string; label: string }[];
className?: string;
classes?: Partial<
Record<'root' | 'trigger' | 'content' | 'item' | 'label', string>
>;
};
export const ColorThemeDisclosure = React.forwardRef<
HTMLButtonElement,
ColorThemeDisclosureProps
>(function ColorThemeDisclosure(
{
label = 'Color theme',
disabled = false,
open,
defaultOpen = false,
onOpenChange,
maxHeight = 240,
direction = 'down',
options: optionOverrides,
animationType,
duration,
slideDirection,
className,
classes,
},
ref
) {
const { colorTheme, colorThemes, mounted, switchColorTheme } = useUITheme();
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const expanded = open ?? internalOpen;
const id = React.useId();
const options =
optionOverrides?.filter((option) => colorThemes.includes(option.value)) ??
colorThemes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
const selected = options.find((option) => option.value === colorTheme);
const style: React.CSSProperties & { '--disclosure-max-height': string } = {
'--disclosure-max-height': `${maxHeight}px`,
};
return (
{label}
);
});
```
## Customization
Use the `classes` prop to style the root, label, trigger, content, and items.
The frame uses theme tokens. Its outer radius equals the trigger radius plus
the frame padding. This control uses solid borders.
Set `animationType` and `duration` for the page transition. Set `slideDirection`
only with `animationType="slide"`. Keep enough space in the expansion direction.
# Theme Toggle · Radix 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 (
);
}
```
### 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 (
);
}
```
### 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 (
);
}
```
## 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 radix --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 (
);
}
```
## 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 as TogglePrimitive } from 'radix-ui';
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;
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 (
);
});
```
## 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.
# Icon Theme Toggle · Radix UI
Animate between sun and moon icons while changing the UI theme.
## 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 { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
### 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 { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
### Slide icons
Sun and moon exchange places with a short sideways movement.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
### Blur icons
A brief blur and crossfade joins the two states.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
## When to use
- Use a compact light/dark action in a header or toolbar.
- Use a short icon animation to make the changed state clear.
## When not to use
- People need to choose the system preference. Use Theme Switcher or Theme Selector.
- The control changes a palette. Use Color Theme Selector.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app. Then install the component.
The CLI copies Icon Theme Toggle and its Theme Toggle dependency. Both reuse the shared `cn` utility.
```sh
npx uitheme-cli add icon-theme-toggle --base radix --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/icon-theme-toggle
```
## Usage
Use the control inside your theme provider. Import it from your app’s component directory.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { IconThemeToggle } from '@/components/ui/icon-theme-toggle';
export function Example() {
return (
);
}
```
## API reference
These props belong to the installed control. Its page transition settings override the provider for this action only.
| 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. |
| effect | 'rotate' | 'slide' | 'blur' | Icon motion only. Defaults to rotate and is separate from the page transition. |
## Keyboard
| Key | Action |
| ------------- | ---------------------------------------------------------------------------------------------- |
| Enter / Space | Toggle the resolved light/dark theme. Keyboard activation changes state without icon movement. |
## Accessibility
- Keep the accessible label when changing the visible content. Icons are decorative.
- The control stays disabled until the theme provider mounts.
- Reduced motion removes travel and rotation. Icon state keeps a short opacity change. The selection surface changes position without travel.
- Keyboard activation changes the theme without the pointer animation.
## Theme behavior
The sun represents resolved light mode. The moon represents resolved dark mode. Activation replaces a system preference with the opposite resolved theme. The clicked button sets the page transition origin.
## AI Agent notes
- Use `uitheme-web/react` for React or `uitheme-web/vue` for Vue. Keep the theme provider around the control.
- Keep page transitions in the control’s `animationType` and `duration` props. Do not add a standalone transition-settings control.
- Keep input labels, focus styles, selected state, disabled state, and reduced-motion behavior when editing source.
- Use the shared theme tokens. Do not hardcode a palette or radius.
## 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 control installs one main component file.
### icon-theme-toggle.tsx
```
'use client';
import * as React from 'react';
import { SunIcon, MoonIcon } from 'lucide-react';
import { useUITheme } from 'uitheme-web/react';
import { ThemeToggle, type ThemeToggleProps } from './theme-toggle';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
export type IconThemeToggleProps = Omit<
ThemeToggleProps,
'display' | 'children' | 'animationType' | 'duration' | 'slideDirection'
> &
ThemeControlTransitionProps & {
effect?: 'rotate' | 'slide' | 'blur';
};
export const IconThemeToggle = React.forwardRef<
HTMLButtonElement,
IconThemeToggleProps
>(function IconThemeToggle(
{ effect = 'rotate', label, classes, onKeyDown, onPointerDown, ...props },
ref
) {
const { resolvedTheme } = useUITheme();
const [keyboard, setKeyboard] = React.useState(false);
const dark = resolvedTheme === 'dark';
const transition = keyboard
? 'transition-none motion-reduce:transition-none'
: 'transition-[transform,translate,rotate,scale,opacity,filter] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-opacity motion-reduce:duration-100';
const hidden = (side: 'sun' | 'moon') =>
effect === 'slide'
? side === 'sun'
? '-translate-x-2'
: 'translate-x-2'
: effect === 'blur'
? 'scale-75 blur-[4px]'
: side === 'sun'
? '-rotate-90 scale-75'
: 'rotate-90 scale-75';
const layer =
'col-start-1 row-start-1 inline-flex size-4 items-center justify-center motion-reduce:!transform-none motion-reduce:!translate-x-0 motion-reduce:!rotate-0 motion-reduce:!scale-100 motion-reduce:!blur-none';
return (
{
setKeyboard(true);
onKeyDown?.(event);
}}
onPointerDown={(event) => {
setKeyboard(false);
onPointerDown?.(event);
}}
classes={{
...classes,
trigger: cn(
'w-10 px-0 active:scale-[0.96] motion-reduce:active:scale-100',
classes?.trigger
),
}}
>
);
});
```
### 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 as TogglePrimitive } from 'radix-ui';
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;
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 (
);
});
```
## 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 `effect="rotate"` (default), `effect="slide"`, or `effect="blur"` for the icon exchange. Icon motion lasts 200ms and retargets from its current state on another pointer activation. Use `animationType`, `duration`, and `slideDirection` for the separate UI theme transition. Use native button props, `label`, and `classes` to customize the control.
# Theme Switcher · Radix UI
Choose system, light, or dark with a moving selection surface.
## 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 { ThemeSwitcher } from '@/components/ui/theme-switcher';
export function Example() {
return (
);
}
```
### 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 { ThemeSwitcher } from '@/components/ui/theme-switcher';
export function Example() {
return (
);
}
```
## When to use
- Show all three appearance choices in a header or settings row.
- Use a moving selection surface to connect the old and new choice.
## When not to use
- Space permits only one compact action. Use Icon Theme Toggle.
- People need a longer list of named choices. Use Theme Selector.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app. Then install the component.
The CLI copies one component file and reuses the shared `cn` utility.
```sh
npx uitheme-cli add theme-switcher --base radix --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-switcher
```
## Usage
Use the control inside your theme provider. Import it from your app’s component directory.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeSwitcher } from '@/components/ui/theme-switcher';
export function Example() {
return (
);
}
```
## API reference
These props belong to the installed control. Its page transition settings override the provider for this action only.
| 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 root, item, and selection indicator 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. |
## Keyboard
| Key | Action |
| ---------- | -------------------------------------- |
| Tab | Enter or leave the radio group. |
| Arrow keys | Move and select among enabled choices. |
| Space | Select the focused radio. |
## Accessibility
- Keep the accessible label when changing the visible content. Icons are decorative.
- The control stays disabled until the theme provider mounts.
- Reduced motion removes travel and rotation. Icon state keeps a short opacity change. The selection surface changes position without travel.
- Keyboard activation changes the theme without the pointer animation.
## Theme behavior
The checked radio follows the stored theme preference, including system. The selected surface moves between the three equal tracks. The icons brighten on fine-pointer hover. The group disables choices that the provider does not support.
## AI Agent notes
- Use `uitheme-web/react` for React or `uitheme-web/vue` for Vue. Keep the theme provider around the control.
- Keep page transitions in the control’s `animationType` and `duration` props. Do not add a standalone transition-settings control.
- Keep input labels, focus styles, selected state, disabled state, and reduced-motion behavior when editing source.
- Use the shared theme tokens. Do not hardcode a palette or radius.
## 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 control installs one main component file.
### theme-switcher.tsx
```
'use client';
import * as React from 'react';
import { useUITheme, type Theme } from 'uitheme-web/react';
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
export type ThemeSwitcherProps = ThemeControlTransitionProps & {
label?: string;
disabled?: boolean;
borderStyle?: 'solid' | 'shadow';
className?: string;
classes?: {
root?: string;
item?: string;
indicator?: string;
};
};
const choices: readonly Theme[] = ['system', 'light', 'dark'];
export function ThemeSwitcher({
label = 'Theme',
disabled = false,
animationType,
duration,
slideDirection,
borderStyle = 'solid',
className,
classes,
}: ThemeSwitcherProps) {
const { theme, themes, mounted, switchTheme } = useUITheme();
const id = React.useId();
const [keyboard, setKeyboard] = React.useState(false);
return (
);
}
```
## 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.
# Theme Selector · Radix UI
Choose light, dark, or the system appearance.
## 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 { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
### 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 { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
### Icon with text
Show an icon beside the theme label.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
## When to use
- Let people choose light, dark, or system appearance in app settings.
- Show the appearance options configured on the provider in one labeled control.
- Use the floating menu in headers, toolbars, and compact layouts.
## When not to use
- A toolbar needs only a quick light/dark action. Use Theme Toggle.
- The choice changes brand colors rather than appearance. 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-selector --base radix --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-selector
```
## 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 { ThemeSelector } from '@/components/ui/theme-selector';
export function Example() {
return (
);
}
```
For inline choices, install [Theme Disclosure](/docs/components/base-ui/theme-disclosure).
The floating selector does not install disclosure code.
## 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 root, trigger, content, item, and label 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` | Open the selector or confirm the focused option. |
| `Arrow keys` | Move between available appearance options. |
| `Escape` | Close the menu and return focus to the trigger. |
## Accessibility
- Keep the label connected to the trigger. Preserve the primitive’s focus behavior.
- The option list comes from the provider. Avoid showing a system option when that option is not configured.
## Theme behavior
- Use `theme` for the selected preference. `System` stays selected when the operating system changes appearance.
- Changes use `switchTheme`. The trigger sets the transition origin. The color palette does not change.
## 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 the provider’s `themes` array. Show only the options configured on the provider.
- Use `theme` for the selected value. Use `resolvedTheme` for visual appearance.
- 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-selector.tsx
```
'use client';
import * as React from 'react';
import { useUITheme } from 'uitheme-web/react';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
import { Select } from 'radix-ui';
export type ThemeSelectorClasses = {
root?: string;
trigger?: string;
content?: string;
item?: string;
label?: string;
};
export type ThemeSelectorProps = ThemeControlTransitionProps & {
display?: 'text' | 'icon-text';
borderStyle?: 'solid' | 'shadow';
className?: string;
classes?: ThemeSelectorClasses;
label?: string;
disabled?: boolean;
};
export const ThemeSelector = React.forwardRef<
HTMLButtonElement,
ThemeSelectorProps
>(function ThemeSelector(
{
animationType,
duration,
slideDirection,
borderStyle = 'solid',
display = 'text',
className,
classes,
label = 'Theme',
disabled = false,
}: ThemeSelectorProps,
forwardedRef
) {
const { theme, themes, mounted, switchTheme } = useUITheme();
const triggerRef = React.useRef(null);
const id = React.useId();
const selected = theme;
const options: {
value: string;
label: string;
}[] = themes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
React.useImperativeHandle(forwardedRef, () => triggerRef.current!);
const change = (
value: string,
element: HTMLElement | null = triggerRef.current
) => {
const next = themes.find((theme) => theme === value);
if (next)
void switchTheme(next, {
element,
animationType,
duration,
slideDirection,
});
};
return (
);
});
```
## 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.
# Color Theme Selector · Radix UI
Choose a color palette without changing the appearance.
## 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 { ColorThemeSelector } from '@/components/ui/color-theme-selector';
export function Example() {
return (
);
}
```
### 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 { ColorThemeSelector } from '@/components/ui/color-theme-selector';
export function Example() {
return (
);
}
```
## When to use
- Let people choose an app palette without changing light or dark appearance.
- Show readable labels for the palette keys configured on the provider.
- Use the floating menu in headers, toolbars, and compact layouts.
## When not to use
- People are choosing light, dark, or system. Use Theme Selector.
- You need a free-form color picker. This control selects registered palette keys.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install the component.
```sh
npx uitheme-cli add color-theme-selector --base radix --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/color-theme-selector
```
## 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 { ColorThemeSelector } from '@/components/ui/color-theme-selector';
export function Example() {
return (
);
}
```
For inline choices, install [Color Theme Disclosure](/docs/components/base-ui/color-theme-disclosure).
The floating selector does not install disclosure code.
## 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 root, trigger, content, item, and label 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. |
| options | { value, label }[] | Change palette labels. Values must already exist in the provider’s colorThemes. |
## Keyboard
| Key | Action |
| --------------- | ------------------------------------------------- |
| `Enter / Space` | Open the selector or confirm the focused palette. |
| `Arrow keys` | Move between palette options. |
| `Escape` | Close the menu and return focus to the trigger. |
## Accessibility
- Use readable option labels instead of relying on color swatches alone.
- Use option values from `colorThemes`. Custom labels do not register new palettes.
## Theme behavior
- The control calls `switchColorTheme`. The appearance preference does not change.
- Register each palette key on the provider. Define its light and dark CSS variables. Selecting a key does not create its colors.
## 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`.
- Keep appearance and `colorTheme` separate. Use the palette action to change colors.
- Check that the selected palette changes the computed semantic colors in light and dark appearance. A changed label alone does not confirm this.
- 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.
### color-theme-selector.tsx
```
'use client';
import * as React from 'react';
import { useUITheme } from 'uitheme-web/react';
import { cn } from '@/lib/utils';
import type { ThemeControlTransitionProps } from 'uitheme-web/react';
import { CheckIcon, ChevronDownIcon } from 'lucide-react';
import { Select } from 'radix-ui';
export type ColorThemeOption = {
value: string;
label: string;
};
export type ColorThemeSelectorClasses = {
root?: string;
trigger?: string;
content?: string;
item?: string;
label?: string;
};
export type ColorThemeSelectorProps = ThemeControlTransitionProps & {
borderStyle?: 'solid' | 'shadow';
className?: string;
classes?: ColorThemeSelectorClasses;
label?: string;
options?: ColorThemeOption[];
disabled?: boolean;
};
export const ColorThemeSelector = React.forwardRef<
HTMLButtonElement,
ColorThemeSelectorProps
>(function ColorThemeSelector(
{
animationType,
duration,
slideDirection,
borderStyle = 'solid',
className,
classes,
label = 'Color theme',
options: optionOverrides,
disabled = false,
}: ColorThemeSelectorProps,
forwardedRef
) {
const { colorTheme, colorThemes, mounted, switchColorTheme } = useUITheme();
const triggerRef = React.useRef(null);
const id = React.useId();
const selected = colorTheme;
const options: {
value: string;
label: string;
}[] =
optionOverrides ??
colorThemes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
React.useImperativeHandle(forwardedRef, () => triggerRef.current!);
const change = (
value: string,
element: HTMLElement | null = triggerRef.current
) =>
switchColorTheme(value, {
element,
animationType,
duration,
slideDirection,
});
return (
);
});
```
## 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.
Pass `animationType`, `duration`, and `slideDirection` to set this control's palette 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.
# Theme Disclosure · Radix UI
Expand appearance choices within the page layout.
## 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 { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
### Opens upward
Place the choices above the trigger and the label below it. Reserved space keeps the preview and trigger in place.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
### Icon with text
Show an icon beside the theme label.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
## When to use
- Show appearance choices in a settings panel with space in the expansion direction.
- Keep the choices open while people compare their selection.
- Use a bounded scroll area for a longer list.
## When not to use
- Expansion would cover or move content in a compact toolbar. Use [Theme Selector](/docs/components/base-ui/theme-selector).
- You need a general content disclosure. This control selects a theme preference.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install this control. The floating selector is a separate component.
```sh
npx uitheme-cli add theme-disclosure --base radix --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-disclosure
```
## Usage
Place the control inside your theme provider. Expansion moves the content below
it. The panel stays open after a selection. Its scroll area defaults to 240px and
never exceeds 60% of the viewport height.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ThemeDisclosure } from '@/components/ui/theme-disclosure';
export function Example() {
return (
);
}
```
Set `direction="up"` to place the choices above the trigger. Leave space above
for the expanded panel. Anchor the control at the bottom of its container if the
trigger must stay in place. The Examples section shows this layout. The default
is `direction="down"`.
Keep the label below the trigger when the panel opens upward. The component
places the label there automatically. Keep the label above a downward-opening
trigger. Preserve this order when you customize the layout.
Reserve enough height for the expanded panel, trigger, label, and frame padding.
Align downward controls to the top and upward controls to the bottom. The
examples reserve `h-96` with the default `maxHeight={240}`. Adjust the space if
you change the scroll limit. Opening a panel must not shift its trigger or the
surrounding preview.
## API reference
Use `open` with the change callback to control expansion. Use `defaultOpen` for
an initial expanded state. Use `maxHeight` to set the scroll limit.
| Prop | Type | Behavior |
| --- | --- | --- |
| label | string | Override the readable accessible label. |
| className | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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. |
| open | boolean | Control whether the inline panel is expanded. |
| defaultOpen | boolean | Start with the panel expanded. Defaults to false. |
| onOpenChange | (open: boolean) => void | Receive the next expanded state when the trigger is pressed. |
| direction | 'down' | 'up' | Place choices below or above the trigger. Defaults to down. Use a bottom-anchored layout to keep an upward trigger in place. |
| maxHeight | number | Limit the scroll area in pixels. Defaults to 240 and never exceeds 60% of the viewport height. |
## Keyboard
| Key | Action |
| --------------- | -------------------------------------------------------------------- |
| `Enter / Space` | Expand or collapse the focused trigger. |
| `Tab` | Move from the trigger into the expanded panel and its radio choices. |
| `Arrow keys` | Change the selected radio within the group. |
| `Space` | Select the focused radio. |
The panel stays open until you collapse it. Escape does not close an inline panel.
## Accessibility
- Keep the visible label, trigger name, and radio group name.
- The trigger reports the expanded state. Hidden choices leave the tab order.
- The disabled state prevents expansion and selection.
- Keyboard controls use native buttons and radio inputs.
## Theme behavior
The checked radio follows `theme`, including `system`. Choices come from `themes`. The color palette does not change.
The selected radio sets the transition origin. The library performs the page
transition. Omitted transition props keep the provider settings.
## AI Agent notes
- Import the local control. Use `uitheme-web/react` or `uitheme-web/vue` for the runtime.
- Read the configured provider choices. Do not invent theme values.
- Preserve the label, focus styles, selected state, disabled state, and scroll limit.
- Keep the label below an upward trigger and above a downward trigger.
- Reserve room for the full bounded panel, trigger, label, and frame padding.
- Anchor upward controls at the bottom and downward controls at the top. Opening the panel must not shift the preview.
- Use the separate floating selector in compact layouts.
- Keep page transitions in the library. Do not add a local transition helper.
- Check keyboard selection, preference persistence, and reduced motion in the running app.
## Source
The CLI reuses the shared `cn` utility. It repairs a missing utility and checks an
existing utility before offering a replacement. This control installs one main
component file. It does not install the floating selector.
### theme-disclosure.tsx
```
'use client';
import * as React from 'react';
import {
useUITheme,
type ThemeControlTransitionProps,
} from 'uitheme-web/react';
import { ChevronDownIcon, SunIcon, MoonIcon, MonitorIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
export type ThemeDisclosureProps = ThemeControlTransitionProps & {
label?: string;
disabled?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
maxHeight?: number;
direction?: 'up' | 'down';
display?: 'text' | 'icon-text';
className?: string;
classes?: Partial<
Record<'root' | 'trigger' | 'content' | 'item' | 'label', string>
>;
};
export const ThemeDisclosure = React.forwardRef<
HTMLButtonElement,
ThemeDisclosureProps
>(function ThemeDisclosure(
{
label = 'Theme',
disabled = false,
open,
defaultOpen = false,
onOpenChange,
maxHeight = 240,
direction = 'down',
display = 'text',
animationType,
duration,
slideDirection,
className,
classes,
},
ref
) {
const { theme, themes, mounted, switchTheme } = useUITheme();
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const expanded = open ?? internalOpen;
const id = React.useId();
const options = themes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
const selected = options.find((option) => option.value === theme);
const style: React.CSSProperties & { '--disclosure-max-height': string } = {
'--disclosure-max-height': `${maxHeight}px`,
};
return (
{label}
);
});
```
## Customization
Use the `classes` prop to style the root, label, trigger, content, and items.
The frame uses theme tokens. Its outer radius equals the trigger radius plus
the frame padding. This control uses solid borders.
Set `animationType` and `duration` for the page transition. Set `slideDirection`
only with `animationType="slide"`. Keep enough space in the expansion direction.
# Color Theme Disclosure · Radix UI
Expand color palette choices within the page layout.
## 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 { ColorThemeDisclosure } from '@/components/ui/color-theme-disclosure';
export function Example() {
return (
);
}
```
### Opens upward
Place the choices above the trigger and the label below it. Reserved space keeps the preview and trigger in place.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ColorThemeDisclosure } from '@/components/ui/color-theme-disclosure';
export function Example() {
return (
);
}
```
## When to use
- Show color palette choices in a settings panel with space in the expansion direction.
- Keep the choices open while people compare their selection.
- Use a bounded scroll area for a longer list.
## When not to use
- Expansion would cover or move content in a compact toolbar. Use [Color Theme Selector](/docs/components/base-ui/color-theme-selector).
- You need a general content disclosure. This control selects a theme preference.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install this control. The floating selector is a separate component.
```sh
npx uitheme-cli add color-theme-disclosure --base radix --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/color-theme-disclosure
```
## Usage
Place the control inside your theme provider. Expansion moves the content below
it. The panel stays open after a selection. Its scroll area defaults to 240px and
never exceeds 60% of the viewport height.
```tsx
'use client';
import { UIThemeProvider } from 'uitheme-web/react';
import { ColorThemeDisclosure } from '@/components/ui/color-theme-disclosure';
export function Example() {
return (
);
}
```
Set `direction="up"` to place the choices above the trigger. Leave space above
for the expanded panel. Anchor the control at the bottom of its container if the
trigger must stay in place. The Examples section shows this layout. The default
is `direction="down"`.
Keep the label below the trigger when the panel opens upward. The component
places the label there automatically. Keep the label above a downward-opening
trigger. Preserve this order when you customize the layout.
Reserve enough height for the expanded panel, trigger, label, and frame padding.
Align downward controls to the top and upward controls to the bottom. The
examples reserve `h-96` with the default `maxHeight={240}`. Adjust the space if
you change the scroll limit. Opening a panel must not shift its trigger or the
surrounding preview.
## API reference
Use `open` with the change callback to control expansion. Use `defaultOpen` for
an initial expanded state. Use `maxHeight` to set the scroll limit.
| Prop | Type | Behavior |
| --- | --- | --- |
| label | string | Override the readable accessible label. |
| className | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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. |
| options | { value, label }[] | Change palette labels. Values must already exist in the provider’s colorThemes. |
| open | boolean | Control whether the inline panel is expanded. |
| defaultOpen | boolean | Start with the panel expanded. Defaults to false. |
| onOpenChange | (open: boolean) => void | Receive the next expanded state when the trigger is pressed. |
| direction | 'down' | 'up' | Place choices below or above the trigger. Defaults to down. Use a bottom-anchored layout to keep an upward trigger in place. |
| maxHeight | number | Limit the scroll area in pixels. Defaults to 240 and never exceeds 60% of the viewport height. |
## Keyboard
| Key | Action |
| --------------- | -------------------------------------------------------------------- |
| `Enter / Space` | Expand or collapse the focused trigger. |
| `Tab` | Move from the trigger into the expanded panel and its radio choices. |
| `Arrow keys` | Change the selected radio within the group. |
| `Space` | Select the focused radio. |
The panel stays open until you collapse it. Escape does not close an inline panel.
## Accessibility
- Keep the visible label, trigger name, and radio group name.
- The trigger reports the expanded state. Hidden choices leave the tab order.
- The disabled state prevents expansion and selection.
- Keyboard controls use native buttons and radio inputs.
## Theme behavior
The checked radio follows `colorTheme`. Choices come from `colorThemes`. Appearance does not change.
The selected radio sets the transition origin. The library performs the page
transition. Omitted transition props keep the provider settings.
## AI Agent notes
- Import the local control. Use `uitheme-web/react` or `uitheme-web/vue` for the runtime.
- Read the configured provider choices. Do not invent theme values.
- Preserve the label, focus styles, selected state, disabled state, and scroll limit.
- Keep the label below an upward trigger and above a downward trigger.
- Reserve room for the full bounded panel, trigger, label, and frame padding.
- Anchor upward controls at the bottom and downward controls at the top. Opening the panel must not shift the preview.
- Use the separate floating selector in compact layouts.
- Keep page transitions in the library. Do not add a local transition helper.
- Check keyboard selection, preference persistence, and reduced motion in the running app.
## Source
The CLI reuses the shared `cn` utility. It repairs a missing utility and checks an
existing utility before offering a replacement. This control installs one main
component file. It does not install the floating selector.
### color-theme-disclosure.tsx
```
'use client';
import * as React from 'react';
import {
useUITheme,
type ThemeControlTransitionProps,
} from 'uitheme-web/react';
import { ChevronDownIcon } from 'lucide-react';
import { cn } from '@/lib/utils';
export type ColorThemeDisclosureProps = ThemeControlTransitionProps & {
label?: string;
disabled?: boolean;
open?: boolean;
defaultOpen?: boolean;
onOpenChange?: (open: boolean) => void;
maxHeight?: number;
direction?: 'up' | 'down';
options?: readonly { value: string; label: string }[];
className?: string;
classes?: Partial<
Record<'root' | 'trigger' | 'content' | 'item' | 'label', string>
>;
};
export const ColorThemeDisclosure = React.forwardRef<
HTMLButtonElement,
ColorThemeDisclosureProps
>(function ColorThemeDisclosure(
{
label = 'Color theme',
disabled = false,
open,
defaultOpen = false,
onOpenChange,
maxHeight = 240,
direction = 'down',
options: optionOverrides,
animationType,
duration,
slideDirection,
className,
classes,
},
ref
) {
const { colorTheme, colorThemes, mounted, switchColorTheme } = useUITheme();
const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
const expanded = open ?? internalOpen;
const id = React.useId();
const options =
optionOverrides?.filter((option) => colorThemes.includes(option.value)) ??
colorThemes.map((value) => ({
value,
label: value
.replaceAll('-', ' ')
.replace(/\b\w/g, (letter) => letter.toUpperCase()),
}));
const selected = options.find((option) => option.value === colorTheme);
const style: React.CSSProperties & { '--disclosure-max-height': string } = {
'--disclosure-max-height': `${maxHeight}px`,
};
return (
{label}
);
});
```
## Customization
Use the `classes` prop to style the root, label, trigger, content, and items.
The frame uses theme tokens. Its outer radius equals the trigger radius plus
the frame padding. This control uses solid borders.
Set `animationType` and `duration` for the page transition. Set `slideDirection`
only with `animationType="slide"`. Keep enough space in the expansion direction.
# Theme Toggle · Vue / Reka
Switch between light and dark with an animated transition.
## Preview
Install the Vue source in your app to preview it with your theme provider.
## Examples
### Theme transition
A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.
```vue
```
### Shadow borders
Use a prominent ambient shadow and inset edge in place of a solid border.
```vue
```
### Icon with text
Show an icon beside the theme label.
```vue
```
## 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 reka --tailwind 4 --language ts
```
## Usage
Use the component inside your theme provider. Use import paths that match your
app’s component directory.
```vue
```
## 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. |
| class | 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/vue` for the runtime. Import the control from its installed local file. The runtime does not export styled controls.
- Use `createUITheme` or `UIThemeProvider` for Vue. In Nuxt 4, use the `uitheme-web/nuxt` module. Follow the provider setup in the installation guide.
- 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.vue
```
{{ text }}
```
## 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.
# Icon Theme Toggle · Vue / Reka
Animate between sun and moon icons while changing the UI theme.
## Preview
Install the Vue source in your app to preview it with your theme provider.
## Examples
### Theme transition
A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.
```vue
```
### Shadow borders
Use a prominent ambient shadow and inset edge in place of a solid border.
```vue
```
### Slide icons
Sun and moon exchange places with a short sideways movement.
```vue
```
### Blur icons
A brief blur and crossfade joins the two states.
```vue
```
## When to use
- Use a compact light/dark action in a header or toolbar.
- Use a short icon animation to make the changed state clear.
## When not to use
- People need to choose the system preference. Use Theme Switcher or Theme Selector.
- The control changes a palette. Use Color Theme Selector.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app. Then install the component.
The CLI copies Icon Theme Toggle and its Theme Toggle dependency. Both reuse the shared `cn` utility.
```sh
npx uitheme-cli add icon-theme-toggle --base reka --tailwind 4 --language ts
```
## Usage
Use the control inside your theme provider. Import it from your app’s component directory.
```vue
```
## API reference
These props belong to the installed control. Its page transition settings override the provider for this action only.
| 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. |
| class | 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. |
| effect | 'rotate' | 'slide' | 'blur' | Icon motion only. Defaults to rotate and is separate from the page transition. |
## Keyboard
| Key | Action |
| ------------- | ---------------------------------------------------------------------------------------------- |
| Enter / Space | Toggle the resolved light/dark theme. Keyboard activation changes state without icon movement. |
## Accessibility
- Keep the accessible label when changing the visible content. Icons are decorative.
- The control stays disabled until the theme provider mounts.
- Reduced motion removes travel and rotation. Icon state keeps a short opacity change. The selection surface changes position without travel.
- Keyboard activation changes the theme without the pointer animation.
## Theme behavior
The sun represents resolved light mode. The moon represents resolved dark mode. Activation replaces a system preference with the opposite resolved theme. The clicked button sets the page transition origin.
## AI Agent notes
- Use `uitheme-web/react` for React or `uitheme-web/vue` for Vue. Keep the theme provider around the control.
- Keep page transitions in the control’s `animationType` and `duration` props. Do not add a standalone transition-settings control.
- Keep input labels, focus styles, selected state, disabled state, and reduced-motion behavior when editing source.
- Use the shared theme tokens. Do not hardcode a palette or radius.
## 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 control installs one main component file.
### icon-theme-toggle.vue
```
```
### theme-toggle.vue
```
{{ text }}
```
## 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 `effect="rotate"` (default), `effect="slide"`, or `effect="blur"` for the icon exchange. Icon motion lasts 200ms and retargets from its current state on another pointer activation. Use `animationType`, `duration`, and `slideDirection` for the separate UI theme transition. Use native button props, `label`, and `classes` to customize the control.
# Theme Switcher · Vue / Reka
Choose system, light, or dark with a moving selection surface.
## Preview
Install the Vue source in your app to preview it with your theme provider.
## Examples
### Theme transition
A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.
```vue
```
### Shadow borders
Use a prominent ambient shadow and inset edge in place of a solid border.
```vue
```
## When to use
- Show all three appearance choices in a header or settings row.
- Use a moving selection surface to connect the old and new choice.
## When not to use
- Space permits only one compact action. Use Icon Theme Toggle.
- People need a longer list of named choices. Use Theme Selector.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app. Then install the component.
The CLI copies one component file and reuses the shared `cn` utility.
```sh
npx uitheme-cli add theme-switcher --base reka --tailwind 4 --language ts
```
## Usage
Use the control inside your theme provider. Import it from your app’s component directory.
```vue
```
## API reference
These props belong to the installed control. Its page transition settings override the provider for this action only.
| 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. |
| class | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, item, and selection indicator 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. |
## Keyboard
| Key | Action |
| ---------- | -------------------------------------- |
| Tab | Enter or leave the radio group. |
| Arrow keys | Move and select among enabled choices. |
| Space | Select the focused radio. |
## Accessibility
- Keep the accessible label when changing the visible content. Icons are decorative.
- The control stays disabled until the theme provider mounts.
- Reduced motion removes travel and rotation. Icon state keeps a short opacity change. The selection surface changes position without travel.
- Keyboard activation changes the theme without the pointer animation.
## Theme behavior
The checked radio follows the stored theme preference, including system. The selected surface moves between the three equal tracks. The icons brighten on fine-pointer hover. The group disables choices that the provider does not support.
## AI Agent notes
- Use `uitheme-web/react` for React or `uitheme-web/vue` for Vue. Keep the theme provider around the control.
- Keep page transitions in the control’s `animationType` and `duration` props. Do not add a standalone transition-settings control.
- Keep input labels, focus styles, selected state, disabled state, and reduced-motion behavior when editing source.
- Use the shared theme tokens. Do not hardcode a palette or radius.
## 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 control installs one main component file.
### theme-switcher.vue
```
```
## 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.
# Theme Selector · Vue / Reka
Choose light, dark, or the system appearance.
## Preview
Install the Vue source in your app to preview it with your theme provider.
## Examples
### Theme transition
A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.
```vue
```
### Shadow borders
Use a prominent ambient shadow and inset edge in place of a solid border.
```vue
```
### Icon with text
Show an icon beside the theme label.
```vue
```
## When to use
- Let people choose light, dark, or system appearance in app settings.
- Show the appearance options configured on the provider in one labeled control.
- Use the floating menu in headers, toolbars, and compact layouts.
## When not to use
- A toolbar needs only a quick light/dark action. Use Theme Toggle.
- The choice changes brand colors rather than appearance. 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-selector --base reka --tailwind 4 --language ts
```
## Usage
Use the component inside your theme provider. Use import paths that match your
app’s component directory.
```vue
```
For inline choices, install [Theme Disclosure](/docs/components/base-ui/theme-disclosure).
The floating selector does not install disclosure code.
## 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. |
| class | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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` | Open the selector or confirm the focused option. |
| `Arrow keys` | Move between available appearance options. |
| `Escape` | Close the menu and return focus to the trigger. |
## Accessibility
- Keep the label connected to the trigger. Preserve the primitive’s focus behavior.
- The option list comes from the provider. Avoid showing a system option when that option is not configured.
## Theme behavior
- Use `theme` for the selected preference. `System` stays selected when the operating system changes appearance.
- Changes use `switchTheme`. The trigger sets the transition origin. The color palette does not change.
## AI Agent notes
Use this checklist when a coding agent adds the control.
- Use `uitheme-web/vue` for the runtime. Import the control from its installed local file. The runtime does not export styled controls.
- Use `createUITheme` or `UIThemeProvider` for Vue. In Nuxt 4, use the `uitheme-web/nuxt` module. Follow the provider setup in the installation guide.
- 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 the provider’s `themes` array. Show only the options configured on the provider.
- Use `theme` for the selected value. Use `resolvedTheme` for visual appearance.
- 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-selector.vue
```
{{
option[0]!.toUpperCase() + option.slice(1)
}}
```
## 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.
# Color Theme Selector · Vue / Reka
Choose a color palette without changing the appearance.
## Preview
Install the Vue source in your app to preview it with your theme provider.
## Examples
### Theme transition
A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.
```vue
```
### Shadow borders
Use a prominent ambient shadow and inset edge in place of a solid border.
```vue
```
## When to use
- Let people choose an app palette without changing light or dark appearance.
- Show readable labels for the palette keys configured on the provider.
- Use the floating menu in headers, toolbars, and compact layouts.
## When not to use
- People are choosing light, dark, or system. Use Theme Selector.
- You need a free-form color picker. This control selects registered palette keys.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install the component.
```sh
npx uitheme-cli add color-theme-selector --base reka --tailwind 4 --language ts
```
## Usage
Use the component inside your theme provider. Use import paths that match your
app’s component directory.
```vue
```
For inline choices, install [Color Theme Disclosure](/docs/components/base-ui/color-theme-disclosure).
The floating selector does not install disclosure code.
## 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. |
| class | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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. |
| options | { value, label }[] | Change palette labels. Values must already exist in the provider’s colorThemes. |
## Keyboard
| Key | Action |
| --------------- | ------------------------------------------------- |
| `Enter / Space` | Open the selector or confirm the focused palette. |
| `Arrow keys` | Move between palette options. |
| `Escape` | Close the menu and return focus to the trigger. |
## Accessibility
- Use readable option labels instead of relying on color swatches alone.
- Use option values from `colorThemes`. Custom labels do not register new palettes.
## Theme behavior
- The control calls `switchColorTheme`. The appearance preference does not change.
- Register each palette key on the provider. Define its light and dark CSS variables. Selecting a key does not create its colors.
## AI Agent notes
Use this checklist when a coding agent adds the control.
- Use `uitheme-web/vue` for the runtime. Import the control from its installed local file. The runtime does not export styled controls.
- Use `createUITheme` or `UIThemeProvider` for Vue. In Nuxt 4, use the `uitheme-web/nuxt` module. Follow the provider setup in the installation guide.
- 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`.
- Keep appearance and `colorTheme` separate. Use the palette action to change colors.
- Check that the selected palette changes the computed semantic colors in light and dark appearance. A changed label alone does not confirm this.
- 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.
### color-theme-selector.vue
```
{{ option.label }}
```
## 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.
Pass `animationType`, `duration`, and `slideDirection` to set this control's palette 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.
# Theme Disclosure · Vue / Reka
Expand appearance choices within the page layout.
## Preview
Install the Vue source in your app to preview it with your theme provider.
## Examples
### Theme transition
A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.
```vue
```
### Opens upward
Place the choices above the trigger and the label below it. Reserved space keeps the preview and trigger in place.
```vue
```
### Icon with text
Show an icon beside the theme label.
```vue
```
## When to use
- Show appearance choices in a settings panel with space in the expansion direction.
- Keep the choices open while people compare their selection.
- Use a bounded scroll area for a longer list.
## When not to use
- Expansion would cover or move content in a compact toolbar. Use [Theme Selector](/docs/components/base-ui/theme-selector).
- You need a general content disclosure. This control selects a theme preference.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install this control. The floating selector is a separate component.
```sh
npx uitheme-cli add theme-disclosure --base reka --tailwind 4 --language ts
```
## Usage
Place the control inside your theme provider. Expansion moves the content below
it. The panel stays open after a selection. Its scroll area defaults to 240px and
never exceeds 60% of the viewport height.
```vue
```
Set `direction="up"` to place the choices above the trigger. Leave space above
for the expanded panel. Anchor the control at the bottom of its container if the
trigger must stay in place. The Examples section shows this layout. The default
is `direction="down"`.
Keep the label below the trigger when the panel opens upward. The component
places the label there automatically. Keep the label above a downward-opening
trigger. Preserve this order when you customize the layout.
Reserve enough height for the expanded panel, trigger, label, and frame padding.
Align downward controls to the top and upward controls to the bottom. The
examples reserve `h-96` with the default `maxHeight={240}`. Adjust the space if
you change the scroll limit. Opening a panel must not shift its trigger or the
surrounding preview.
## API reference
Use `open` with the change callback to control expansion. Use `defaultOpen` for
an initial expanded state. Use `maxHeight` to set the scroll limit.
| Prop | Type | Behavior |
| --- | --- | --- |
| label | string | Override the readable accessible label. |
| class | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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. |
| open | boolean | Control whether the inline panel is expanded. |
| defaultOpen | boolean | Start with the panel expanded. Defaults to false. |
| update:open | event | Receive the next expanded state when the trigger is pressed. |
| direction | 'down' | 'up' | Place choices below or above the trigger. Defaults to down. Use a bottom-anchored layout to keep an upward trigger in place. |
| maxHeight | number | Limit the scroll area in pixels. Defaults to 240 and never exceeds 60% of the viewport height. |
## Keyboard
| Key | Action |
| --------------- | -------------------------------------------------------------------- |
| `Enter / Space` | Expand or collapse the focused trigger. |
| `Tab` | Move from the trigger into the expanded panel and its radio choices. |
| `Arrow keys` | Change the selected radio within the group. |
| `Space` | Select the focused radio. |
The panel stays open until you collapse it. Escape does not close an inline panel.
## Accessibility
- Keep the visible label, trigger name, and radio group name.
- The trigger reports the expanded state. Hidden choices leave the tab order.
- The disabled state prevents expansion and selection.
- Keyboard controls use native buttons and radio inputs.
## Theme behavior
The checked radio follows `theme`, including `system`. Choices come from `themes`. The color palette does not change.
The selected radio sets the transition origin. The library performs the page
transition. Omitted transition props keep the provider settings.
## AI Agent notes
- Import the local control. Use `uitheme-web/react` or `uitheme-web/vue` for the runtime.
- Read the configured provider choices. Do not invent theme values.
- Preserve the label, focus styles, selected state, disabled state, and scroll limit.
- Keep the label below an upward trigger and above a downward trigger.
- Reserve room for the full bounded panel, trigger, label, and frame padding.
- Anchor upward controls at the bottom and downward controls at the top. Opening the panel must not shift the preview.
- Use the separate floating selector in compact layouts.
- Keep page transitions in the library. Do not add a local transition helper.
- Check keyboard selection, preference persistence, and reduced motion in the running app.
## Source
The CLI reuses the shared `cn` utility. It repairs a missing utility and checks an
existing utility before offering a replacement. This control installs one main
component file. It does not install the floating selector.
### theme-disclosure.vue
```
{{ label }}
```
## Customization
Use the `classes` prop to style the root, label, trigger, content, and items.
The frame uses theme tokens. Its outer radius equals the trigger radius plus
the frame padding. This control uses solid borders.
Set `animationType` and `duration` for the page transition. Set `slideDirection`
only with `animationType="slide"`. Keep enough space in the expansion direction.
# Color Theme Disclosure · Vue / Reka
Expand color palette choices within the page layout.
## Preview
Install the Vue source in your app to preview it with your theme provider.
## Examples
### Theme transition
A slide transition declared on this control. Duration is 500 milliseconds. Other controls keep their own settings.
```vue
```
### Opens upward
Place the choices above the trigger and the label below it. Reserved space keeps the preview and trigger in place.
```vue
```
## When to use
- Show color palette choices in a settings panel with space in the expansion direction.
- Keep the choices open while people compare their selection.
- Use a bounded scroll area for a longer list.
## When not to use
- Expansion would cover or move content in a compact toolbar. Use [Color Theme Selector](/docs/components/base-ui/color-theme-selector).
- You need a general content disclosure. This control selects a theme preference.
## Installation
Set up [uitheme-web](/docs/installation) in an existing Tailwind app.
Then install this control. The floating selector is a separate component.
```sh
npx uitheme-cli add color-theme-disclosure --base reka --tailwind 4 --language ts
```
## Usage
Place the control inside your theme provider. Expansion moves the content below
it. The panel stays open after a selection. Its scroll area defaults to 240px and
never exceeds 60% of the viewport height.
```vue
```
Set `direction="up"` to place the choices above the trigger. Leave space above
for the expanded panel. Anchor the control at the bottom of its container if the
trigger must stay in place. The Examples section shows this layout. The default
is `direction="down"`.
Keep the label below the trigger when the panel opens upward. The component
places the label there automatically. Keep the label above a downward-opening
trigger. Preserve this order when you customize the layout.
Reserve enough height for the expanded panel, trigger, label, and frame padding.
Align downward controls to the top and upward controls to the bottom. The
examples reserve `h-96` with the default `maxHeight={240}`. Adjust the space if
you change the scroll limit. Opening a panel must not shift its trigger or the
surrounding preview.
## API reference
Use `open` with the change callback to control expansion. Use `defaultOpen` for
an initial expanded state. Use `maxHeight` to set the scroll limit.
| Prop | Type | Behavior |
| --- | --- | --- |
| label | string | Override the readable accessible label. |
| class | string | Extend the root styles. Your classes replace default classes through the local merge helper. |
| classes | object | Style root, trigger, content, item, and label 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. |
| options | { value, label }[] | Change palette labels. Values must already exist in the provider’s colorThemes. |
| open | boolean | Control whether the inline panel is expanded. |
| defaultOpen | boolean | Start with the panel expanded. Defaults to false. |
| update:open | event | Receive the next expanded state when the trigger is pressed. |
| direction | 'down' | 'up' | Place choices below or above the trigger. Defaults to down. Use a bottom-anchored layout to keep an upward trigger in place. |
| maxHeight | number | Limit the scroll area in pixels. Defaults to 240 and never exceeds 60% of the viewport height. |
## Keyboard
| Key | Action |
| --------------- | -------------------------------------------------------------------- |
| `Enter / Space` | Expand or collapse the focused trigger. |
| `Tab` | Move from the trigger into the expanded panel and its radio choices. |
| `Arrow keys` | Change the selected radio within the group. |
| `Space` | Select the focused radio. |
The panel stays open until you collapse it. Escape does not close an inline panel.
## Accessibility
- Keep the visible label, trigger name, and radio group name.
- The trigger reports the expanded state. Hidden choices leave the tab order.
- The disabled state prevents expansion and selection.
- Keyboard controls use native buttons and radio inputs.
## Theme behavior
The checked radio follows `colorTheme`. Choices come from `colorThemes`. Appearance does not change.
The selected radio sets the transition origin. The library performs the page
transition. Omitted transition props keep the provider settings.
## AI Agent notes
- Import the local control. Use `uitheme-web/react` or `uitheme-web/vue` for the runtime.
- Read the configured provider choices. Do not invent theme values.
- Preserve the label, focus styles, selected state, disabled state, and scroll limit.
- Keep the label below an upward trigger and above a downward trigger.
- Reserve room for the full bounded panel, trigger, label, and frame padding.
- Anchor upward controls at the bottom and downward controls at the top. Opening the panel must not shift the preview.
- Use the separate floating selector in compact layouts.
- Keep page transitions in the library. Do not add a local transition helper.
- Check keyboard selection, preference persistence, and reduced motion in the running app.
## Source
The CLI reuses the shared `cn` utility. It repairs a missing utility and checks an
existing utility before offering a replacement. This control installs one main
component file. It does not install the floating selector.
### color-theme-disclosure.vue
```
{{ label }}
```
## Customization
Use the `classes` prop to style the root, label, trigger, content, and items.
The frame uses theme tokens. Its outer radius equals the trigger radius plus
the frame padding. This control uses solid borders.
Set `animationType` and `duration` for the page transition. Set `slideDirection`
only with `animationType="slide"`. Keep enough space in the expansion direction.