Provider setup
The one mount-once wrapper your app needs — what it wires, where it goes, and every way it can go wrong.
Last updated
VegaStackProvider is the app-root wrapper that supplies the invisible plumbing
many components depend on. Install and mount it before adding any other
component — several failure modes below are silent.
pnpm dlx shadcn@latest add @vegastack/providerimport { VegaStackProvider } from "@/components/ui/provider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<VegaStackProvider>{children}</VegaStackProvider>
</body>
</html>
);
}What it wires
| Wiring | Without the provider you get |
|---|---|
Dark mode — next-themes, attribute="class", system-aware, no-flash | toggles that do nothing; .dark never reaches <html> |
Toasts — ToastProvider plus the Toaster portal | toast(...) calls that silently do nothing — no error anywhere |
Tooltip coordination — Base UI Tooltip.Provider | every tooltip re-waits its full open delay; toolbars feel broken |
Text direction — Base UI DirectionProvider | RTL layouts break |
Full component reference: Provider.
The three rules
- Exactly once, at the root. Never nest a second provider — the bundled Toaster is a mount-once portal and double-mounting double-renders every toast.
suppressHydrationWarningon<html>. next-themes sets the theme class on the client before hydration; without the attribute React logs hydration mismatch warnings on every load.- If something else already mounts a
<Toaster />, suppress the bundled one:<VegaStackProvider toaster={false}>— or replace it:toaster={<Toaster position="top-center" />}.
Theme toggle (copy-paste)
"use client";
import { Moon, Sun } from "lucide-react";
import { useVegaStackTheme } from "@/components/ui/provider";
import { Button } from "@/components/ui/button";
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useVegaStackTheme();
const isDark = resolvedTheme === "dark";
return (
<Button
variant="ghost"
size="md"
aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
onClick={() => setTheme(isDark ? "light" : "dark")}
>
{isDark ? <Sun /> : <Moon />}
</Button>
);
}resolvedTheme is undefined until mounted (SSR can't know the system theme) —
the component above is hydration-safe as written; don't branch on it during
render for anything layout-affecting.
Theme choice persists in localStorage automatically and follows the OS setting
until the user picks explicitly (defaultTheme="system").
Non-Next.js apps
The provider is plain React — in Vite/anything else, wrap your root render
instead of a layout file. The suppressHydrationWarning note is Next-specific
(it exists because of SSR); pure-CSR apps don't need it.
Symptom → cause table
| Symptom | Cause |
|---|---|
toast('…') does nothing, zero errors | no provider (or it's below the component calling toast) |
| Toasts appear twice | two Toasters — a nested provider or an extra manual <Toaster />; use toaster={false} |
Hydration-mismatch warning mentioning <html> class | missing suppressHydrationWarning |
| Dark mode flashes wrong theme on load | provider not at the root layout, or a competing theme script |
| Tooltips each wait ~600ms even in a toolbar | no provider (no shared Tooltip.Provider) |
Assembling a multi-step form
Compose Stepper with Field, per-step validation, and async advance gating — the wizard recipe the components deliberately don't own.
Theming, dark mode & tokens
The one-file override model, the base a11y layer you must not skip, and consuming the token contract outside React.