createThemes
Typed factory for ThemeProvider and theme hooks.
Last updated on
createThemes lets you define your themes once and get typed client provider/hooks everywhere.
"use client";
import { createThemes } from "@wrksz/themes/client";
export const { ThemeProvider, useTheme, useThemeValue, useThemeEffect, ThemedImage } =
createThemes({
themes: ["light", "dark", "high-contrast"] as const,
defaultTheme: "system",
attribute: "class",
storage: "hybrid",
disableTransitionOnChange: true,
themeColor: { light: "#ffffff", dark: "#0f1115" },
});Why use it
- You define the theme tuple once, and type inference propagates to all consumers.
setThemeaccepts only valid values from your configured theme union.useThemeValuemaps are checked against your union (plus optionaldefault).ThemedImagerequires a source for every configured theme.- Provider defaults can still be overridden per usage, while the theme tuple stays fixed.
- The returned
ThemeProvideris the default client provider. Use@wrksz/themes/nextor@wrksz/themes/next/create-themesfor the root Next.js provider that injects the anti-flash script. themeRoot,systemThemeMap, andenableSameDocumentSyncare not on the factory. Compose with@wrksz/themes/client/extended-providerwhen a subtree needs those props.
"use client";
import { ThemedImage, useTheme, useThemeValue } from "@/app/theme";
export function ThemeToggle() {
const { setTheme } = useTheme();
const label = useThemeValue({
light: "Use dark",
dark: "Use high contrast",
"high-contrast": "Use light",
});
return (
<>
<button onClick={() => setTheme("high-contrast")}>{label}</button>
<ThemedImage
src={{
light: "/logo-light.svg",
dark: "/logo-dark.svg",
"high-contrast": "/logo-high-contrast.svg",
}}
alt="Logo"
/>
</>
);
}Next.js root provider
For App Router layouts, import the same factory from @wrksz/themes/next/create-themes. One config object yields the client pieces and a NextThemeProvider that injects the anti-flash script, so the layout does not re-list themes.
"use client";
import { createThemes } from "@wrksz/themes/next/create-themes";
export const {
ThemeProvider,
NextThemeProvider,
ThemeScript,
useTheme,
useThemeValue,
useThemeEffect,
ThemedImage,
} = createThemes({
themes: ["light", "dark", "high-contrast"] as const,
defaultTheme: "system",
storage: "hybrid",
disableTransitionOnChange: true,
});import { NextThemeProvider } from "@/app/theme";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<NextThemeProvider>{children}</NextThemeProvider>
</body>
</html>
);
}createNextThemes is the same function if you prefer that name. Keep using ThemeProvider from @wrksz/themes/next when you are not using the factory: that entry stays a thin wrapper and does not pull factory code.
ThemeScript on the factory result is for non-Next SSR. It is typed to the same tuple. Next apps should prefer NextThemeProvider (useServerInsertedHTML) over mounting ThemeScript by hand.
Return value
createThemes(config) from @wrksz/themes/client returns:
ThemeProvideruseThemeuseThemeValueuseThemeEffectThemedImage
createThemes(config) from @wrksz/themes/next/create-themes returns those plus:
NextThemeProvider: root Next.js provider with the anti-flash script, bound to the same context asuseThemeThemeScript: typed bootstrap for non-Next SSR
Exported types
The factory exports helper types when you want to type local wrappers or shared maps:
import {
createThemes,
type CreateThemesConfig,
type CreateThemesResult,
type ThemeValueMap,
type TypedThemedImageProps,
} from "@wrksz/themes/client";
const themes = ["light", "dark", "high-contrast"] as const;
type AppTheme = (typeof themes)[number];
const config = {
themes,
defaultTheme: "system",
storage: "hybrid",
} satisfies CreateThemesConfig<typeof themes>;
export const themeApi = createThemes(config);
export type AppThemeApi = CreateThemesResult<typeof themes>;
export type AppThemeValueMap<Value> = ThemeValueMap<AppTheme, Value>;
export type AppThemedImageProps = TypedThemedImageProps<AppTheme>;The Next factory also exports CreateNextThemesResult.
Per-usage overrides
Factory defaults are just defaults. You can override runtime props per mount:
"use client";
import { ThemeProvider } from "@/app/theme";
export function AuthSection({ children }: { children: React.ReactNode }) {
return <ThemeProvider defaultTheme="light">{children}</ThemeProvider>;
}The configured themes tuple remains the source of truth, so the typed provider does not accept a per-use themes override. Create another factory if a subtree genuinely needs a different theme union.
Independent theme dimensions
Every factory owns its own context. You can combine independent mode, palette, typography, or contrast axes without one nested provider hiding another factory's hook:
import { createThemes } from "@wrksz/themes/client";
export const mode = createThemes({
themes: ["light", "dark"] as const,
attribute: "data-mode",
storageKey: "mode",
});
export const palette = createThemes({
themes: ["ocean", "forest"] as const,
attribute: "data-palette",
storageKey: "palette",
enableSystem: false,
});<mode.ThemeProvider>
<palette.ThemeProvider>
{children}
</palette.ThemeProvider>
</mode.ThemeProvider>The resulting attributes are independent, for example data-mode="dark" and data-palette="forest". This avoids defining every mode/palette combination as a separate theme.
Generic useTheme<T>() vs createThemes
- Use
createThemesfor shipped app code with one canonical theme configuration. - Use
useTheme<T>()for quick experiments and local prototypes.
Leaving beta
Short 2.0 checklist for factory users:
- Default vs extended:
createThemesalways binds the default provider.systemThemeMap,themeRoot, andenableSameDocumentSyncstay on@wrksz/themes/next/extendedand@wrksz/themes/client/extended-provider. - Hybrid / SSR: the pre-paint bootstrap reads the cookie. The Next provider does not call
cookies(). PassinitialThemefromgetThemeonly when server markup must know the theme. - React 18+ is the public support floor (native
useEffectEventon React 19.2+).