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.add(...) 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 choice in the user menu
Theme is a preference, so it lives where the other preferences do — in the user menu — as a radio
group of Light, Dark and System. There is no theme-toggle component to install and no
local toggle file to copy: the menu is composed from dropdown-menu and useVegaStackTheme().
"use client";
import { useVegaStackTheme } from "@/components/ui/provider";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
export function UserMenu({ name }: { name: string }) {
const { theme, setTheme } = useVegaStackTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger render={<Button variant="ghost" />}>
{name}
</DropdownMenuTrigger>
<DropdownMenuContent className="w-48">
<DropdownMenuGroup>
<DropdownMenuLabel>{name}</DropdownMenuLabel>
<DropdownMenuItem>Profile</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuSub>
<DropdownMenuSubTrigger>Theme</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuRadioGroup
value={theme ?? "system"}
onValueChange={(value) => setTheme(value)}
>
<DropdownMenuRadioItem value="light">
Light
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="system">
System
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuSeparator />
<DropdownMenuItem>Sign out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}Bind the group to theme (the user's choice, including "system"), never to resolvedTheme (the
theme actually painted): a user who picked System should see System checked. theme is
undefined until the provider mounts, so the group falls back to "system" — the default — and
nothing layout-affecting branches on it during render. A command palette can offer the same three
choices as items that call setTheme.
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.add({ … }) 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) |