Provider setup
Use one root FujiProvider. Theme, material, radius, and elevation are global by design - there is no per-component override. Each accepts a controlled prop, an uncontrolled default* prop, and an on*Change callback. For the full explanation of what each setting does, see Theming.
Basic setup
Set uncontrolled defaults and pass persist on the root provider to save the combined preference across navigation and refresh. theme accepts "light" | "dark", material "solid" | "glass", radius "cornered" | "soft", and elevation "regular" | "floating". Material is independent of theme: glass follows the active theme, so a light theme gives light glass and a dark theme gives dark glass.
import { FujiProvider } from "@fujiui/react";
<FujiProvider
persist
defaultTheme="light"
defaultMaterial="solid"
defaultRadius="cornered"
defaultElevation="regular"
>
<App />
</FujiProvider>persist saves all four settings together under the fuji-appearance key in localStorage. A preference saved by an older version as {"theme":"glass"} is read as dark + glass.
Avoid a flash on load
With persist, the provider reads the saved appearance after hydration, so a returning visitor briefly sees the defaults. buildAppearanceBootstrapScript returns a small script that reads the same storage key and sets the four data-fuji-* attributes on <html> before the first paint. Pass it the same defaults as the root provider and run it before your app: in the Next.js App Router with next/script and strategy="beforeInteractive", or in Vite as an inline <script> in index.html before the app bundle.
// app/layout.tsx
import Script from "next/script";
import { buildAppearanceBootstrapScript, FujiProvider } from "@fujiui/react";
import "@fujiui/react/styles.css";
const APPEARANCE = { theme: "light", material: "solid", radius: "cornered", elevation: "regular" } as const;
const BOOTSTRAP = buildAppearanceBootstrapScript(APPEARANCE);
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" data-fuji-boot="" suppressHydrationWarning>
<body>
<Script id="fuji-appearance" strategy="beforeInteractive" dangerouslySetInnerHTML={{ __html: BOOTSTRAP }} />
<FujiProvider
persist
defaultTheme={APPEARANCE.theme}
defaultMaterial={APPEARANCE.material}
defaultRadius={APPEARANCE.radius}
defaultElevation={APPEARANCE.elevation}
>
{children}
</FujiProvider>
</body>
</html>
);
}data-fuji-boot stops colors animating during that one swap, and suppressHydrationWarning is needed because the script changes <html> before React hydrates. The script validates what it reads and ignores anything it does not recognize. Without persist nothing is saved, so there is no flash and no script to add.
Controlled vs uncontrolled
Pass a controlled value prop (theme, material, radius, elevation) with its on*Change callback to own the state yourself, or pass the default* prop to let the provider manage it. Do not mix both for the same axis.
import { FujiProvider } from "@fujiui/react";
// Controlled: you own the value
<FujiProvider
theme={theme}
onThemeChange={setTheme}
material={material}
onMaterialChange={setMaterial}
defaultRadius="soft"
>
<App />
</FujiProvider>Reading the active config
useFujiConfig() returns the active values and their setters from anywhere inside the provider.
import { useFujiConfig } from "@fujiui/react";
function AppearanceLabel() {
const { theme, material, radius, elevation, setTheme, setMaterial, setRadius, setElevation } =
useFujiConfig();
return (
<span>
{theme} · {material} · {radius} · {elevation}
</span>
);
}Components work without a FujiProvider ancestor too: they fall back to light + solid + cornered + regular, so a single component can be dropped into an existing app before wiring up the provider.
