---
title: Redpanda Provider
description: A single root provider that wires up theming, motion config, and the Base UI toaster for any consumer app.
icon: layers
search:
  keywords: [provider, setup, root]
mode: wide
---

### Installation

Requires React and React DOM **19.3 or newer**. The included Toaster uses the registry's Base UI Toast manager.

```package-install
npx shadcn@latest add @redpanda/redpanda-provider
```

## When to use

Mount `RedpandaProvider` once near the root of your application. It bundles the three things every Redpanda UI consumer otherwise has to wire up by hand:

- **`ThemeProvider`** — manages light/dark/system theme and persists the choice to `localStorage`. See
  [theme switching](#theme-switching) below: it owns the whole contract, so nothing in your app writes
  `data-theme`, the `.dark` class or `color-scheme` itself.
- **`MotionConfig`** with `reducedMotion="user"` — components animated with `motion/react` automatically respect the operating system's `prefers-reduced-motion` setting. Without this, animations play even for users who have requested reduced motion.
- **`Toaster`** — the global Base UI surface so client code can call `toast.add()` from anywhere. See [Toast](/docs/toast).

If you already have your own theme provider or toast system, see the [composition](#composition) section for how to opt out of individual layers.

## Usage

Mount the provider once at the root of your app, above the `RouterProvider`:

```tsx title="src/App.tsx"
import { RouterProvider } from "@tanstack/react-router";

import { RedpandaProvider } from "@/components/redpanda-ui/components/redpanda-provider";

import { router } from "./router";

export const App = () => (
  <RedpandaProvider>
    <RouterProvider router={router} />
  </RedpandaProvider>
);
```

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultTheme` | `'light' \| 'dark' \| 'system'` | `'system'` | Theme to use when no preference is stored in `localStorage`. |
| `themeStorageKey` | `string \| null` | `'redpanda-ui-theme'` | `localStorage` key under which the active theme is persisted. `null` keeps the theme in memory: nothing is stored or picked up from other tabs, for a provider that follows a theme something else owns. |
| `reducedMotion` | `'user' \| 'always' \| 'never'` | `'user'` | How motion components respond to `prefers-reduced-motion`. `'user'` honors the OS preference (recommended). `'always'` forces animations off — useful for visual regression tests. `'never'` disables the check entirely. |
| `toaster` | `false \| ToasterProps` | `{}` | Props forwarded to the Base UI `Toaster`, including `timeout`, `limit`, `container`, and `toastManager`. Pass `false` when your app already mounts its own Base UI host. |

## Upgrading the toast host

The default host now uses Base UI, not Sonner. Move calls to `toast.add()` and replace Sonner-specific `toaster` options using the [migration guide](/docs/toast#migrating-from-sonner). Remove any additional host using the same Base UI manager, or notifications will render twice. `toaster={false}` still opts out.

The deprecated Sonner component remains installable. If existing calls must keep working during migration, mount its `Toaster` explicitly beside `RedpandaProvider`; it is no longer mounted implicitly.

## Composition

`RedpandaProvider` is a thin wrapper around three primitives. If you need finer control — for example, to swap the theme provider for `next-themes` or to mount the toaster in a custom location — compose them directly instead:

```tsx
import { MotionConfig } from "motion/react";

import { Toaster } from "@/components/redpanda-ui/components/toast";
import { ThemeProvider } from "@/components/redpanda-ui/components/theme-provider";

export function AppProviders({ children }: { children: React.ReactNode }) {
  return (
    <ThemeProvider defaultTheme="system">
      <MotionConfig reducedMotion="user">
        {children}
        <Toaster />
      </MotionConfig>
    </ThemeProvider>
  );
}
```

:::warning
If you swap `ThemeProvider` for something else, that something has to write `data-theme` — with
`next-themes` that means `attribute="data-theme"`, since its default is a class the theme only reads as
a compatibility selector. See [theme switching](#theme-switching) for the rest of the contract it takes
over.
:::

## Theme switching

`ThemeProvider` writes the three things that together are what being in a theme means, so no app code
should set any of them by hand:

| written on `<html>` | who reads it |
| --- | --- |
| `data-theme="light \| dark"` | `theme.css` — one attribute the page owns, so a theme cannot be half-applied |
| `class="dark"` | CSS the theme does not own: a stylesheet you vendored before the move, a docs shell, Tailwind's own class-based `dark:` in your app code |
| `color-scheme` | the *browser* — scrollbars, native form controls, date pickers, the canvas behind an overscroll |

`'system'` resolves against `prefers-color-scheme` and keeps following it while the tab is open; a change
made in another tab applies here too.

```tsx
import { useTheme } from "@/components/redpanda-ui/components/theme-provider";

const { theme, resolvedTheme, setTheme } = useTheme();
// theme:         'light' | 'dark' | 'system' — the choice
// resolvedTheme: 'light' | 'dark'            — what the page is actually in
```

Reach for `resolvedTheme` for anything that has to branch on the *rendered* theme — a two-state toggle, a
Monaco theme, an image swap — since `'system'` is not something you can paint.

### Before the first paint

The provider can only apply the stored theme from its mount effect, which is one paint too late: a
server-rendered page shows the light ground and then flips. Two entry points do it earlier, both sharing
the provider's resolve so they cannot disagree with it:

```tsx title="Server-rendered (Next.js) — a blocking inline script"
import { ThemeScript } from "@/components/redpanda-ui/components/theme-provider";

<html lang="en" suppressHydrationWarning>
  <body>
    <ThemeScript defaultTheme="dark" storageKey="my-app:theme" />
    …
```

```tsx title="Client-rendered (Vite) — the entry module, above createRoot"
import { initTheme } from "@/components/redpanda-ui/components/theme-provider";

initTheme({ defaultTheme: "dark", storageKey: "my-app:theme" });
```

`ThemeScript` has to run before anything themed paints, so it goes in `<head>` or first inside `<body>` —
first-in-`<body>` above, because a Next App Router layout owns `<head>` itself. `suppressHydrationWarning`
goes on `<html>`, since mutating the element the server rendered is the script's whole job.

Pass both the same `storageKey` and `defaultTheme` as the provider, or the two disagree for one paint.
`themeScript()` is also exported, for a framework that wants the source as a string.

## Reduced motion

`motion/react` does **not** honor `prefers-reduced-motion` by default — its built-in default is `'never'`. The `reducedMotion="user"` setting in this provider opts your app in to that accessibility preference, so animated registry components (Tabs, Auto Form, animated panels, etc.) collapse transform-based animations to instant transitions when the user has Reduce Motion enabled at the OS level. Opacity transitions are kept, which is WCAG-compliant.
