# Agent integration guide
This guide covers `@wrksz/themes` 2.x. Check your installed version and type declarations; online docs may include APIs from a newer release.
For a shorter reference, ask your agent to read `node_modules/@wrksz/themes/AGENTS.md`. Some agents skip instructions inside dependencies.
[Plain Markdown](/llms.mdx/docs/agents) · [Documentation index](/llms.txt) · [Full documentation](/llms-full.txt)
## Imports [#imports]
| Use case | Import | What it provides |
| ------------------------------------------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Next.js server root layout | `ThemeProvider` from `@wrksz/themes/next` | Bootstrap plus client theme state |
| Client component | Hooks or `ClientThemeProvider` from `@wrksz/themes/client` | Read/update state or create a nested scope |
| Typed Next.js theme module | `createThemes` from `@wrksz/themes/next/create-themes` | Export `NextThemeProvider` and matching hooks from one factory |
| React without Next.js | `ClientThemeProvider` from `@wrksz/themes/client` | Client theme state |
| Other React SSR frameworks | Also render `ThemeScript` from `@wrksz/themes/script` | Apply the initial theme before hydration |
| Custom system mappings or same-document synchronization | `@wrksz/themes/next/extended` or `@wrksz/themes/client/extended-provider` | Extended provider features |
| Client-owned element or ShadowRoot | `ClientThemeProvider` from `@wrksz/themes/client/extended-provider` | Set `themeRoot` in client code |
The package root exports the client provider and factory without a bootstrap script. For Next.js, use `/next` in server layouts. That entry also exports server-only `getTheme`, so client modules must use `/client` or `/next/create-themes`.
## Next.js layout and toggle [#nextjs-layout-and-toggle]
Keep the root layout on the server and leave existing auth or query providers in place.
```tsx title="app/layout.tsx"
import type { ReactNode } from "react";
import { ThemeProvider } from "@wrksz/themes/next";
export default function RootLayout({ children }: { children: ReactNode }) {
return (
{children}
);
}
```
```tsx title="app/theme-toggle.tsx"
"use client";
import { useTheme } from "@wrksz/themes/client";
export function ThemeToggle() {
const { resolvedTheme, forcedTheme, setTheme } = useTheme();
return (
);
}
```
The button renders the same label before hydration. For icons or text that depend on the theme, render a placeholder until `useHydrated()` returns true. `suppressHydrationWarning` on `html` covers only that element's attribute changes.
The default provider uses `class`, `localStorage`, key `theme`, light/dark themes and system detection. CSS must match the chosen attribute; the provider does not supply styles.
## Typed configuration [#typed-configuration]
```tsx title="app/theme.ts"
"use client";
import { createThemes } from "@wrksz/themes/next/create-themes";
export const { NextThemeProvider, useTheme, useThemeValue } = createThemes({
themes: ["light", "dark", "high-contrast"] as const,
attribute: "class",
defaultTheme: "system",
});
```
Import the provider and hooks from this same module. Each factory creates a separate context, so global hooks cannot read its provider. Call `createThemes` once at module scope.
Use the [extended provider](/docs/api/theme-provider) for `systemThemeMap`, `themeRoot` or `enableSameDocumentSync`; the factory does not accept these options. Custom system names such as `paper`/`midnight` need `systemThemeMap` on the extended provider, or `enableSystem={false}` with an explicit default.
## Storage and server rendering [#storage-and-server-rendering]
The Next.js provider applies the stored theme in the browser without calling `cookies()` on the server. Choose storage based on where you need the preference:
* `localStorage` retains the preference and synchronizes browser tabs.
* `sessionStorage` retains it for the tab; the library does not subscribe to cross-tab updates.
* `cookie` makes it readable by the server but does not synchronize tabs.
* `hybrid` reads cookies first and mirrors writes to localStorage for cross-tab updates.
* `none` avoids persistence, useful for independent widgets or forced themes.
`getTheme()` reads cookies, not localStorage, and cannot determine the browser's system preference. It can return `"system"`, even with `defaultTheme: "light"` when the stored cookie contains `"system"`. Do not copy an unresolved selection directly into `html.className`.
If server-rendered content needs the theme, read the cookie per request and pass the selection as `initialTheme`. With Cache Components, keep that read inside the app's request-time boundary. See [getTheme](/docs/api/get-theme) and [server themes](/docs/examples/server-theme).
In other SSR frameworks, render `ThemeScript` before theme-dependent content and match its `themes`, `attribute`, `value`, `storage`, `storageKey` and defaults to the client provider. See [framework integration](/docs/examples/framework-agnostic).
## Antipatterns [#antipatterns]
| Avoid | Use instead |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Replacing imports without inspecting existing CSS | Preserve `attribute` and `value`; `next-themes` defaults to `data-theme`, this package to `class` |
| Moving `@wrksz/themes/next` into a client wrapper | Render it in the server layout; keep unrelated client providers nested inside |
| Wrapping the same root in two theme providers | Use one root provider; give independent scopes their own target and storage key |
| Calling `setTheme` with an undeclared custom name | Add the name to `themes`; invalid selections are ignored |
| Rendering `resolvedTheme` as if it always exists | Handle `undefined` before hydration |
| Passing ordinary callback functions from a Server Component | Define `onThemeChange` and `onStorageError` in a client module; a typed Next factory can hold client callbacks |
| Using `initialTheme` as a controlled prop | Use `setTheme` for selection; initialization runs at mount |
| Assuming `forcedTheme` saves the user's preference | It overrides the active theme without writing storage; disable switching UI |
| Assuming `useTheme().themes` includes `system` | Render the system option separately when `enableSystem` is enabled |
| Treating `followSystem` as ordinary system support | `enableSystem` offers a system option; `followSystem` ignores stored selection and follows later system changes |
Changing `initialTheme` or `storageKey` does not reload stored state. To initialize again after switching accounts or workspaces, remount the provider.
## Troubleshooting [#troubleshooting]
| Symptom | Inspect first |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `useTheme must be used within its ThemeProvider` | Check that the hook is below the matching provider and that the app uses one installed copy of the package |
| Build error involving `next/headers` in a client module | Replace the client module's `/next` import with `/client` or `/next/create-themes` |
| Theme changes in state but not visually | CSS selectors, `attribute`, mapped values, target existence and competing providers |
| Custom theme selection does nothing | The `themes` allowlist, `forcedTheme`, and custom system mappings |
| Flash or hydration warning | Initial HTML contains the bootstrap; CSP permits it; CSS selectors match; server and first client markup agree |
| Theme returns to an old value on mount | `initialTheme` overrides storage; hybrid reads cookie before localStorage; inspect the actual keys |
| No synchronization between tabs | Storage mode, same origin and storage key; cookie/session/none do not use the cross-tab listener |
| No synchronization between providers in one page | Use the extended provider with `enableSameDocumentSync`; same key alone is insufficient |
| Storage access fails | Browser restrictions or cookie options; observe `onStorageError` from client code |
With CSP, pass the request's script nonce to authorize the bootstrap. It does not need `unsafe-eval`. Transition suppression also creates a style element; check `style-src` separately because a script nonce does not authorize styles.
## Migration and verification [#migration-and-verification]
Follow the [migration guide](/docs/migration) before removing `next-themes`.
Run the app's type check and production build. Test cold loads with empty and saved storage, then reload and use browser back/forward. Exercise the system option and any forced or scoped themes. For localStorage/hybrid, test a second tab; for extended same-document sync, test two providers.
Report the checks you ran in the consuming app, including any CSS, CSP or hydration issues that remain.
# Getting Started
These docs cover **2.x**. Install `@wrksz/themes` without a prerelease tag. See [Upgrading from 1.x](/docs/migration#upgrading-from-1x).
## Installation [#installation]
```bash
npm install @wrksz/themes
```
```bash
pnpm add @wrksz/themes
```
```bash
yarn add @wrksz/themes
```
## Setup [#setup]
Add `ThemeProvider` to your root layout. Import from `@wrksz/themes/next` for Next.js - this avoids the React 19 inline script warning by using `useServerInsertedHTML`. Add `suppressHydrationWarning` to `` to prevent hydration mismatches.
```tsx title="app/layout.tsx"
import { ThemeProvider } from "@wrksz/themes/next";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
## Usage [#usage]
Use the `useTheme` hook in any Client Component to read and update the current theme.
```tsx title="components/theme-toggle.tsx"
"use client";
import { useTheme } from "@wrksz/themes/client";
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useTheme();
return (
);
}
```
## Import paths [#import-paths]
| Import | Use for |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `@wrksz/themes/next` | `ThemeProvider` in Next.js (recommended) |
| `@wrksz/themes/client` | `useTheme`, `useThemeValue`, `useThemeEffect`, `ThemedImage`, `ClientThemeProvider`, `createThemes` |
| `@wrksz/themes/client/use-theme` | Direct `useTheme` import |
| `@wrksz/themes/client/use-theme-value` | Direct `useThemeValue` import |
| `@wrksz/themes/client/use-theme-effect` | Direct `useThemeEffect` import |
| `@wrksz/themes/client/themed-image` | Direct `ThemedImage` import |
| `@wrksz/themes/client/provider` | Direct `ClientThemeProvider` import |
| `@wrksz/themes/client/create-themes` | Direct `createThemes` import |
| `@wrksz/themes/next/create-themes` | Typed `createThemes` with `NextThemeProvider` and `ThemeScript` |
| `@wrksz/themes/next/extended` | Opt-in Next.js `ThemeProvider` with `systemThemeMap` and same-document sync |
| `@wrksz/themes/client/extended-provider` | Opt-in `ClientThemeProvider` with `themeRoot`, `systemThemeMap`, and same-document sync |
| `@wrksz/themes` | Client-safe `ThemeProvider` alias and `createThemes` for framework-neutral React usage |
The API is identical to `next-themes`. Migrating requires changing one import line: `@wrksz/themes/next` instead of `next-themes`. See [Why not next-themes?](/docs/why-not-next-themes) for a full comparison.
## Requirements [#requirements]
* Next.js 16+
* React 18+ (uses native `useEffectEvent` on React 19.2+)
* TypeScript 5.9+ (`typescript` peer is `>=5.9` from `2.0.0`; 4.x and 5.0–5.8 are unsupported on this line)
## Next steps [#next-steps]
* [ThemeProvider API](/docs/api/theme-provider) - all available props
* [useTheme API](/docs/api/use-theme) - hook reference
* [Examples](/docs/examples) - Tailwind, scoped theming, [Shadow DOM](/docs/examples/shadow-dom), and more
# Migrating from next-themes
To migrate to `@wrksz/themes` 2.x, update the imports and check your CSS selectors and provider placement. The [agent guide](/docs/agents) covers integration errors and testing.
## Step 1: Install [#step-1-install]
These instructions cover **2.x**. If you use 1.x, also follow [Upgrading from 1.x](#upgrading-from-1x).
```bash
npm install @wrksz/themes
```
```bash
pnpm add @wrksz/themes
```
```bash
yarn add @wrksz/themes
```
## Step 2: Update the provider import [#step-2-update-the-provider-import]
```diff
- import { ThemeProvider } from "next-themes";
+ import { ThemeProvider } from "@wrksz/themes/next";
```
Preserve the existing `themes`, `value`, `storageKey`, `defaultTheme` and system settings. `next-themes` defaults to `data-theme`; `@wrksz/themes` defaults to `class`. If your CSS uses `[data-theme="dark"]`, retain it explicitly:
```tsx
{children}
```
Keep `attribute="class"` if the app already uses it. Keeping `storage="localStorage"` and the same `storageKey` preserves saved preferences.
These defaults are documented in the [next-themes API](https://github.com/pacocoursey/next-themes#themeprovider). Check the application's installed version and any local wrapper before migrating.
## Upgrading from 1.x [#upgrading-from-1x]
These changes apply when upgrading from **1.x to 2.x**. Skip this section if you already use 2.x.
### What broke vs 1.2.0 [#what-broke-vs-120]
| Change | 1.x | 2.0.0 |
| ----------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| Next `ThemeProvider` | `async`, called `cookies()` for `storage="cookie"\|"hybrid"` and set `initialTheme` for you | Synchronous; does not read cookies on the server. |
| Server cookie read | Provider called `cookies()` for cookie/hybrid storage | Call [`getTheme`](/docs/api/get-theme) explicitly when server content needs the selection; pass `initialTheme` to seed the provider |
| TypeScript peer | `>=4.5.0` | `>=5.9` (5.0–5.8 unsupported) |
| `forcedTheme` | Could write the forced value into storage on init | Does **not** persist to storage |
| `initialTheme` / `storageKey` | Prop changes could re-run mount init | Initializes at mount; later prop changes do not re-initialize |
### Migration path [#migration-path]
* For zero-flash without request-time APIs, keep `storage="cookie"` or `"hybrid"`. The pre-paint bootstrap still reads the cookie before hydration.
* If server-rendered markup must know the theme, do this explicitly:
```tsx title="app/layout.tsx"
import { ThemeProvider, getTheme } from "@wrksz/themes/next";
export const instant = false;
export default async function RootLayout({ children }) {
const theme = await getTheme();
return (
{children}
);
}
```
## Step 3: Move the root provider to the server layout [#step-3-move-the-root-provider-to-the-server-layout]
`@wrksz/themes/next` is the server-layout entry and also exports the server-only `getTheme` helper. Move the root theme provider out of a client wrapper. Keep unrelated auth, query or UI providers in that wrapper and render it inside the new root theme provider.
In 1.x this entry was an **async** Server Component. From `2.0.0` it is synchronous, but it still must not be imported from a `"use client"` module because it re-exports `getTheme`.
```diff
- "use client";
- import { ThemeProvider } from "next-themes";
- export function Providers({ children }) {
- return {children};
- }
```
```diff title="app/layout.tsx"
+ import { ThemeProvider } from "@wrksz/themes/next";
export default function RootLayout({ children }) {
return (
+ {children}
);
}
```
If you need a nested provider inside a Client Component, use [`ClientThemeProvider`](/docs/api/client-theme-provider) instead.
## Step 4: Update client hook imports [#step-4-update-client-hook-imports]
```diff
- import { useTheme } from "next-themes";
+ import { useTheme } from "@wrksz/themes/client";
```
## API differences [#api-differences]
Check your props against the [provider reference](/docs/api/theme-provider):
* `themes` is an allowlist: declare every custom name passed to `setTheme` or `forcedTheme`.
* `useTheme().themes` returns the configured list without automatically appending `"system"`. Add a system menu option explicitly when system support is enabled.
* `theme` and `resolvedTheme` may be `undefined` before hydration. Preserve hydration guards for theme-dependent UI.
* A nested provider creates its own state. Use distinct DOM targets and storage keys for independent sections.
* The root package entry is client-only. Use `/next` for the Next.js root bootstrap and `/client` for client hooks.
### `onThemeChange` [#onthemechange]
`onThemeChange` is an additional `@wrksz/themes` callback, not part of the documented `next-themes` provider API. If the old app used a wrapper with that name, inspect its behavior before replacing it.
Calling `setTheme("system")` reports the selected value `"system"`. A later system-preference change while following the system reports the resolved value (`"light"` or `"dark"`). Handle both when persisting preferences.
Define callbacks in client code; do not pass an ordinary function from a Server Component through the Next.js provider. A client module using the [Next.js typed factory](/docs/api/create-themes#nextjs-root-provider) can configure a callback while exporting a root `NextThemeProvider`.
### `disableTransitionOnChange` [#disabletransitiononchange]
Accepts `boolean | string`. Passing a CSS `transition` string suppresses only those specific properties, keeping other transitions intact.
```tsx
// next-themes
// @wrksz/themes - also accepts a CSS string
```
## New features [#new-features]
* `storage="hybrid"` - cookie-first reads for SSR + `localStorage` mirror for cross-tab sync.
* `storage="cookie"` - persists the selection in a cookie; the bootstrap reads it before hydration.
* `storage="sessionStorage"` - persists theme only for the current tab.
* `storage="none"` - no persistence, useful for scoped or forced themes.
* `themeColor` - updates `` on theme change.
* `initialTheme` - initialize theme from a server-side source.
* `followSystem` - always follow system preference, ignoring stored value.
* `getTheme()` - read the current theme from a cookie outside React.
* `createThemes(...)` - creates a provider and hooks from one typed theme list. Use `@wrksz/themes/next/create-themes` when the root layout needs `NextThemeProvider` without re-listing the tuple.
* `useThemeEffect(...)` - side effects that run on theme changes after mount.
See [Why not next-themes?](/docs/why-not-next-themes) for the full comparison.
## Verify before removing next-themes [#verify-before-removing-next-themes]
Search the application for remaining `next-themes` imports, including wrappers and tests. Remove the old dependency with the project's package manager only after replacing them.
Run the app's type check and production build. Test loading with empty and saved storage, then reload and use back/forward navigation. Check custom themes and forced routes if the app uses them. Confirm that CSS selectors match the DOM, hydration produces no warnings, and theme menus still offer the system option when enabled.
# Why not next-themes?
`@wrksz/themes` is a near drop-in replacement for `next-themes`. It fixes every known bug and adds missing features. The API is identical - migrating requires changing one import line. Shadow DOM (`themeRoot`) and custom system maps (`systemThemeMap`) are opt-in extended entries, not default bundle weight.
`next-themes` is slowly maintained - 43 open issues and 16 open PRs as of March 2026, with React 19 compatibility bugs still unresolved in the latest release.
## Bug fixes [#bug-fixes]
### React 19 script warning [#react-19-script-warning]
`next-themes` renders an inline `