Skip to content
Redpanda UI
Esc
↑↓navigate↵open⌘Jpreview
Edit on GitHub

Redpanda Provider

A single root provider that wires up theming, motion config, and the Base UI toaster for any consumer app.

Installation

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

bash
npx shadcn@latest add @redpanda/redpanda-provider
bash
pnpm dlx shadcn@latest add @redpanda/redpanda-provider
bash
yarn dlx shadcn@latest add @redpanda/redpanda-provider
bash
bunx shadcn@latest add @redpanda/redpanda-provider
bash
nubx shadcn@latest add @redpanda/redpanda-provider
bash
aube dlx 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 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.

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

Usage

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

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. 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>  );}

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:

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" />    …
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.

Recent changes

  • patchv1.2.0Pin shipped dependency floors to the version we develop against. Registry items now declare ranges like `^5.1.9` (the actual installed version) instead of collapsing to `^5.0.0`, so consumers start on the known-tested baseline while caret semantics still allow any compatible release within the same major.#133
  • minorv1.1.0Theme docs refresh, readability pass on semantic foregrounds, and consumer-facing Base UI regression fixes.#121
  • minorv1.0.0Post-Base-UI polish. Public API unchanged.#116
  • majorv1.0.0Migrate every Radix-based primitive to `@base-ui/react@^1.4.0` (Base UI).#114
  • minorv0.3.0Add theme-provider component to the registry with documentation and tests. Includes playground type improvements (export RegistryItem, remove as-const boilerplate) and docs site dark mode border color fix.#109
See full history →

Last updated on

Was this page helpful?