Skip to main content

Theme Provider

BetaSince 0.1.0

Light, dark or system theme for the whole app, remembered across visits and open tabs.

import { ThemeProvider, useTheme } from "@anantya/uix";

Overview

ThemeProvider gives the whole app a light, dark or system theme. It follows the device's setting until the reader picks a theme, then remembers the choice across visits and open tabs. Mount one in the root layout, and read or change the theme anywhere below it with useTheme().

For a button that switches between light and dark, use Theme Toggle, which uses useTheme() for you.

Signature

function ThemeProvider(props: ThemeProviderProps): ReactElement;

interface ThemeProviderProps {
children?: ReactNode;
defaultTheme?: ThemeMode;
storageKey?: string;
nonce?: string;
tabbableLinks?: boolean;
}

function useTheme(): UseThemeReturn;

interface UseThemeReturn {
theme: ThemeMode;
resolvedTheme: ColorScheme;
setTheme: (theme: ThemeMode) => void;
}

type ThemeMode = "light" | "dark" | "system";
type ColorScheme = "light" | "dark";

Parameters

PropTypeDefaultDescription
childrenReactNode-The app.
defaultTheme"light" | "dark" | "system""system"The theme until the reader picks one. system follows the device's setting.
storageKeystring"theme"The localStorage key that keeps the reader's choice.
noncestring-The nonce for its inline script and style, when your Content Security Policy allows them by nonce.
tabbableLinksbooleantrueGives links a tabindex="0", so Tab reaches them in Safari, which skips links by default.

useTheme() takes no parameters.

Returns

ThemeProvider renders its children, after a LiveRegion, so announce() works anywhere in the app. On the server it also renders a small inline script that sets the theme before the first paint, so the page never shows the wrong one.

useTheme() returns:

NameTypeDescription
theme"light" | "dark" | "system"The reader's choice, or defaultTheme until they make one.
resolvedTheme"light" | "dark"The theme shown, with system resolved to the device's setting. Changes when that setting does.
setTheme(theme) => voidApplies a theme and saves it as the reader's choice, for every open tab.

useTheme() throws an error when there is no ThemeProvider above it.

Example

Mount ThemeProvider inside <body> in your root layout, such as app/layout.tsx in Next.js:

import { ThemeProvider } from "@anantya/uix";
import type { ReactNode } from "react";
import "./globals.css";

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}

On a settings page, a Switch sets system with useTheme(), beside a Theme Toggle for light and dark. Each switches this whole page.

Showing the light theme

Code
"use client";

import { Switch, ThemeToggle, useTheme } from "@anantya/uix";

export default function MatchDevice() {
const { theme, resolvedTheme, setTheme } = useTheme();
return (
<div className="flex flex-col gap-3 text-sm">
<label className="flex items-center gap-3 font-medium">
<Switch
checked={theme === "system"}
onCheckedChange={(checked) => setTheme(checked ? "system" : resolvedTheme)}
/>
Match my device
</label>
<p className="flex items-center gap-2 text-muted-foreground">
Showing the {resolvedTheme} theme
<ThemeToggle size="sm" variant="secondary" />
</p>
</div>
);
}

Notes

  • ThemeProvider sets the dark class on <html>. The page's HTML from the server doesn't have it, so give <html> suppressHydrationWarning, as above
  • On the server and until hydration is done, useTheme() returns defaultTheme, and resolvedTheme is light for system. Show what depends on the theme with dark: classes instead, which are right from the first paint
  • When the browser blocks localStorage, the choice lasts until the reader leaves the page
  • A change cross-fades the page once, where the browser has View Transitions. It is instant when the reader prefers reduced motion
  • Mount one ThemeProvider per app

Changelog

Every change to Theme Provider since 0.1.0 is in the release notes.

© Anantya TechLabs. All rights reserved.