@wrksz/themesv2.0.2
API Reference

createThemes

Typed factory for ThemeProvider and theme hooks.

Edit on GitHub

Last updated on

createThemes lets you define your themes once and get typed client provider/hooks everywhere.

app/theme.ts
"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.
  • setTheme accepts only valid values from your configured theme union.
  • useThemeValue maps are checked against your union (plus optional default).
  • ThemedImage requires a source for every configured theme.
  • Provider defaults can still be overridden per usage, while the theme tuple stays fixed.
  • The returned ThemeProvider is the default client provider. Use @wrksz/themes/next or @wrksz/themes/next/create-themes for the root Next.js provider that injects the anti-flash script.
  • themeRoot, systemThemeMap, and enableSameDocumentSync are not on the factory. Compose with @wrksz/themes/client/extended-provider when a subtree needs those props.
app/theme-toggle.tsx
"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.

app/theme.ts
"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,
});
app/layout.tsx
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:

  • ThemeProvider
  • useTheme
  • useThemeValue
  • useThemeEffect
  • ThemedImage

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 as useTheme
  • ThemeScript: typed bootstrap for non-Next SSR

Exported types

The factory exports helper types when you want to type local wrappers or shared maps:

app/theme.ts
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:

app/preferences.ts
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 createThemes for 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: createThemes always binds the default provider. systemThemeMap, themeRoot, and enableSameDocumentSync stay on @wrksz/themes/next/extended and @wrksz/themes/client/extended-provider.
  • Hybrid / SSR: the pre-paint bootstrap reads the cookie. The Next provider does not call cookies(). Pass initialTheme from getTheme only when server markup must know the theme.
  • React 18+ is the public support floor (native useEffectEvent on React 19.2+).

On this page