# Advanced Usage
Custom Themes [#custom-themes]
You can define custom themes beyond light and dark:
```tsx
{children}
```
Use the `value` prop to map theme names to different attribute values:
```tsx
{children}
```
Multiple Attributes [#multiple-attributes]
You can apply themes to multiple attributes simultaneously:
```tsx
{children}
```
This is useful when you need both class-based styling (for Tailwind) and data attributes (for custom CSS).
Forced Themes [#forced-themes]
Force a specific theme for a page or section:
```tsx
{children}
```
When `forcedTheme` is set, it overrides user preferences and localStorage. This is useful for:
* Landing pages that should always be light
* Documentation sections that should always be dark
* A/B testing different themes
Disable System Theme [#disable-system-theme]
Disable system theme detection:
```tsx
{children}
```
When `enableSystem` is false, the `"system"` option is removed from available themes.
Storage Options [#storage-options]
By default, Better Themes uses `localStorage` to persist theme preferences across all tabs. You can switch to `sessionStorage` for per-tab theme isolation:
```tsx
{children}
```
localStorage vs sessionStorage [#localstorage-vs-sessionstorage]
| Storage Type | Behavior |
| ------------------------ | ---------------------- |
| `localStorage` (default) | Shared across all tabs |
| `sessionStorage` | Isolated per tab |
When to use sessionStorage [#when-to-use-sessionstorage]
Use `sessionStorage` when you need different themes in different tabs. For example, in a multi-user environment where multiple users are logged into different accounts in separate tabs, each user can have their own theme preference.
Custom Storage Key [#custom-storage-key]
Change the key used to store theme preferences:
```tsx
{children}
```
Useful when you have multiple theme providers in the same app or want to avoid conflicts with other libraries.
Disable Color Scheme [#disable-color-scheme]
Disable the `color-scheme` CSS property:
```tsx
{children}
```
The `color-scheme` property tells browsers which color scheme to use for native UI elements. You might want to disable it if you're handling this manually.
Disable Transitions [#disable-transitions]
Disable CSS transitions when switching themes:
```tsx
{children}
```
This prevents flashy transitions when themes change. Better Themes temporarily disables all transitions, applies the theme, then re-enables them.
Content Security Policy (CSP) [#content-security-policy-csp]
If you're using CSP headers, provide a nonce for the inline script:
```tsx
{children}
```
The nonce is passed to the inline script that prevents flash on load.
Programmatic Theme Control [#programmatic-theme-control]
Control themes programmatically:
```tsx twoslash
import { useTheme } from "better-themes";
// ---cut---
function MyComponent() {
const { theme, setTheme, themes, systemTheme } = useTheme();
// Set theme directly
setTheme("dark");
// Set theme with callback
setTheme((current) => (current === "dark" ? "light" : "dark"));
// Access all available themes
console.log(themes); // ["light", "dark", "system"]
// Access system preference
console.log(systemTheme); // "dark" or "light"
}
```
Conditional Rendering [#conditional-rendering]
Safely render theme-dependent content:
```tsx
import { useTheme } from "better-themes";
import { useEffect, useState } from "react";
function ThemedContent() {
const [mounted, setMounted] = useState(false);
const { theme } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return
Loading...
;
}
return (
{theme === "dark" && }
{theme === "light" && }
);
}
```
TypeScript [#typescript]
Better Themes is fully typed. Import types for your own components:
```tsx
import type { ThemeProviderProps, UseThemeProps } from "better-themes";
function CustomProvider(props: ThemeProviderProps) {
// ...
}
function useCustomTheme(): UseThemeProps {
// ...
}
```
# API Reference
ThemeProvider [#themeprovider]
The main provider component that wraps your application.
Props [#props]
Default Values [#default-values]
* `themes`: `["light", "dark"]`
* `defaultTheme`: `"system"` (if `enableSystem` is true), otherwise `"light"`
* `storage`: `"localStorage"`
* `storageKey`: `"theme"`
* `enableSystem`: `true`
* `enableColorScheme`: `true`
* `attribute`: `"class"`
* `disableTransitionOnChange`: `false`
Examples [#examples]
**Basic usage:**
```tsx title="layout.tsx"
{children}
```
**With custom themes:**
```tsx title="layout.tsx"
{children}
```
**With data attribute:**
```tsx title="layout.tsx"
{children}
```
**Disable transitions on change:**
```tsx title="layout.tsx"
{children}
```
**Force a specific theme:**
```tsx title="layout.tsx"
{children}
```
**Use sessionStorage for per-tab themes:**
```tsx title="layout.tsx"
{children}
```
useTheme Hook [#usetheme-hook]
Returns theme state and controls.
Return Value [#return-value]
```tsx
interface UseThemeProps {
/** List of all available theme names */
themes: string[];
/** Forced theme name for the current page */
forcedTheme?: string;
/** Update the theme */
setTheme: Dispatch>;
/** Active theme name */
theme?: string;
/** System theme preference ("dark" or "light") */
systemTheme?: "dark" | "light";
}
```
Properties [#properties]
* `theme` - Current active theme name (`undefined` on server)
* `setTheme` - Function to update theme (accepts string or callback)
* `forcedTheme` - Forced theme if set, otherwise `undefined`
* `themes` - Array of all available themes (includes `"system"` if enabled)
* `systemTheme` - System preference (`"dark"` or `"light"`, only if `enableSystem` is true)
Examples [#examples-1]
**Basic usage:**
```tsx title="ThemeToggle.tsx" twoslash
import { useTheme } from "better-themes";
// ---cut---
const { theme, setTheme } = useTheme();
setTheme("dark");
```
**With callback:**
```tsx title="ThemeToggle.tsx" twoslash
import { useTheme } from "better-themes";
// ---cut---
const { theme, setTheme } = useTheme();
setTheme((current) => (current === "dark" ? "light" : "dark"));
```
**Access system theme:**
```tsx title="ThemeToggle.tsx" twoslash
import { useTheme } from "better-themes";
// ---cut---
const { systemTheme } = useTheme();
console.log(systemTheme); // "dark" or "light"
```
Types [#types]
Attribute [#attribute]
```tsx
type Attribute = `data-${string}` | "class";
```
The HTML attribute type that can be modified. Can be `"class"` or any `data-*` attribute.
ValueObject [#valueobject]
```tsx
interface ValueObject {
[themeName: string]: string;
}
```
Mapping of theme names to HTML attribute values. Useful when you want different attribute values than theme names.
Example [#example]
```tsx title="layout.tsx"
{children}
```
RSC Export [#rsc-export]
For Next.js App Router and other React Server Components frameworks, use the `/rsc` export:
```tsx title="layout.tsx"
import { ThemeProvider, useTheme } from "better-themes/rsc";
```
This ensures proper server component compatibility. The API is identical to the main export.
# Getting Started
Better Themes is a framework-agnostic theming solution for React applications. It provides seamless dark mode support with zero flash on load, system preference detection, and flexible storage options.
Features [#features]
* **Zero flash on load** - Prevents theme flash during page load (SSR/SSG safe)
* **System preference detection** - Automatically detects and respects user's system theme preference via `prefers-color-scheme`
* **Storage support** - Support for `localStorage` (default) and `sessionStorage` (per-tab isolation)
* **Themed browser UI** - Sets `color-scheme` CSS property for native browser UI elements
* **Custom themes** - Support for multiple custom themes beyond light/dark
* **Flexible styling** - Use class or data attributes (works with Tailwind CSS)
* **TypeScript** - Fully typed with TypeScript
* **Framework agnostic** - Works with Next.js, Remix / React Router, Vite, TanStack Start, Waku, and more
Quick Start [#quick-start]
Install better-themes:
npm
pnpm
yarn
bun
```bash
npm install better-themes
```
```bash
pnpm add better-themes
```
```bash
yarn add better-themes
```
```bash
bun add better-themes
```
Wrap your app with `ThemeProvider`:
```tsx title="layout.tsx"
import { ThemeProvider } from "better-themes";
function Layout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Use the theme in your components:
```tsx title="theme-switcher.tsx"
import { useTheme } from "better-themes";
function theme-switcher() {
const { theme, setTheme } = useTheme();
return (
);
}
```
This project is deployed on Netlify
Next Steps [#next-steps]
# Installation
Package Managers [#package-managers]
Install better-themes using your preferred package manager:
npm
pnpm
yarn
bun
```bash
npm install better-themes
```
```bash
pnpm add better-themes
```
```bash
yarn add better-themes
```
```bash
bun add better-themes
```
Requirements [#requirements]
* React 17.0.0 or higher (or React 18.0.0 or React 19.0.0)
* React DOM 17.0.0 or higher (or React DOM 18.0.0 or React DOM 19.0.0)
Peer Dependencies [#peer-dependencies]
Better Themes has the following peer dependencies:
* `react`: `^17.0.0 || ^18.0.0 || ^19.0.0`
* `react-dom`: `^17.0.0 || ^18.0.0 || ^19.0.0`
These are automatically installed by most package managers, but you may need to install them manually if you're using a different setup.
Next Steps [#next-steps]
After installation, check out the [Quick Start](/docs/quick-start) guide to get started with better-themes.
# Quick Start
Basic Setup [#basic-setup]
Wrap your app with ThemeProvider [#wrap-your-app-with-themeprovider]
The `ThemeProvider` component should be placed at the root of your application:
```tsx title="layout.tsx"
import { ThemeProvider } from "better-themes";
function Layout({ children }: { children: React.ReactNode }) {
return (
{children}
);
}
```
Add suppressHydrationWarning to your tag [#add-suppresshydrationwarning-to-your-html-tag]
This prevents hydration warnings when the theme is applied:
```tsx title="layout.tsx"
{children}
```
Style your app [#style-your-app]
By default, `ThemeProvider` sets a `class` attribute on the `` element. Style your app with CSS:
```css title="global.css"
:root {
--background: white;
--foreground: black;
}
.dark {
--background: black;
--foreground: white;
}
```
Or use hard-coded values:
```css title="global.css"
html,
body {
color: #000;
background: #fff;
}
.dark,
.dark body {
color: #fff;
background: #000;
}
```
Using the Theme [#using-the-theme]
Access the current theme and change it with the `useTheme` hook:
```tsx title="theme-switcher.tsx"
import { useTheme } from "better-themes";
function theme-switcher() {
const { theme, setTheme, themes } = useTheme();
return (
Current theme: {theme}
);
}
```
Tailwind CSS [#tailwind-css]
For Tailwind CSS, use class-based dark mode:
```tsx title="layout.tsx"
{children}
```
Then use Tailwind's dark mode classes:
```tsx title="page.tsx"
Hello
```
Important Notes [#important-notes]
* The theme value is `undefined` on the server. Only use it after the component mounts on the client.
* See the [SSR Guide](/docs/ssr) for details on handling server-side rendering.
* Check out [Framework Guides](/docs/frameworks/next-app) for framework-specific setup instructions.
# SSR & Hydration
Understanding SSR Behavior [#understanding-ssr-behavior]
Better Themes is designed to work seamlessly with server-side rendering. The theme value is `undefined` on the server to prevent hydration mismatches.
The Problem [#the-problem]
When using SSR, the server doesn't know the user's theme preference. If you render theme-dependent UI on the server, it may not match what's rendered on the client, causing hydration errors.
The Solution [#the-solution]
Only render theme-dependent UI after the component mounts on the client:
```tsx
import { useEffect, useState } from "react";
import { useTheme } from "better-themes";
function ThemeSwitcher() {
const [mounted, setMounted] = useState(false);
const { theme, setTheme, themes } = useTheme();
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return
Loading...
; // or a skeleton
}
return (
);
}
```
Best Practices [#best-practices]
1. Use a mounted state [#1-use-a-mounted-state]
Always check if the component has mounted before rendering theme-dependent UI:
```tsx
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) {
return null; // or a loading state
}
```
2. Provide fallback UI [#2-provide-fallback-ui]
Show a neutral loading state or skeleton while waiting for hydration:
```tsx
if (!mounted) {
return ;
}
```
3. Use CSS for initial styles [#3-use-css-for-initial-styles]
Use CSS to handle initial theme styles, not JavaScript:
```css
:root {
background: white;
color: black;
}
[data-theme='dark'] {
background: black;
color: white;
}
```
4. Avoid theme checks in render [#4-avoid-theme-checks-in-render]
Don't conditionally render based on theme before mounting:
```tsx
// Bad - theme is undefined on server
{theme === "dark" && }
// Good - check mounted first
{mounted && theme === "dark" && }
```
Zero Flash Strategy [#zero-flash-strategy]
Better Themes prevents flash by injecting a script before React hydrates. This script:
1. Reads the theme from localStorage
2. Applies the theme immediately
3. Prevents any flash of unstyled content
The script runs synchronously before the page renders, ensuring zero flash.
Framework-Specific Notes [#framework-specific-notes]
Next.js App Router [#nextjs-app-router]
Use the `/rsc` export for proper server component support:
```tsx twoslash
import { ThemeProvider } from "better-themes/rsc";
```
Next.js Pages Router [#nextjs-pages-router]
Use the default export:
```tsx
import { ThemeProvider } from "better-themes";
```
Other Frameworks [#other-frameworks]
All frameworks use the default export. The SSR behavior is consistent across all React frameworks.
# Theme Switchers
Here are some examples of beautiful theme switchers you can use in your application. We provide implementation using **Shadcn UI** and a **Custom** implementation without external UI libraries.
Shadcn UI [#shadcn-ui]
These examples require `lucide-react` icons and Shadcn UI components.
Radio Group Switcher [#radio-group-switcher]
Uses `RadioGroup` and `Label` components.
Component [#component]
```tsx title="components/radio-switcher.tsx"
"use client";
import { Monitor, Moon, Sun } from "lucide-react";
import { useTheme } from "better-themes";
import { useEffect, useState } from "react";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { cn } from "@/lib/utils";
const themeOptions = [
{ value: "dark", icon: Moon, label: "Dark" },
{ value: "light", icon: Sun, label: "Light" },
{ value: "system", icon: Monitor, label: "System" },
];
export function RadioSwitcher() {
const { theme, setTheme } = useTheme();
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return (
setTheme(value) : undefined}
className="flex items-center gap-2"
>