Themer is a theme management library for React applications built for TanStack Router and TanStack Start
README generated by Claude Code, curated by @lukonik
โ Zero Flash of Unstyled Content (FOUC) - Theme applied before React hydration
โ SSR Support - Works seamlessly with server-side rendering
โ Client Support - Works seamlessly with SPA (Single Page Applications)
โ System Theme Detection - Automatically follows OS dark/light mode preferences
โ Different Storage Types - Save theme with built-in localStorage, sessionStorage, cookieStorage, or write your own custom storage adapter
โ Cross-Tab Synchronization - Theme changes sync across browser tabs
โ Flexible Theme Application - Apply themes via data attributes or CSS classes
โ Custom Theme Values - Map theme names to custom attribute values
โ TypeScript Support - Fully typed API
โ Lightweight - Minimal bundle size with no external dependencies (except peer deps)
โ No Transition Flash - Optionally disable CSS transitions during theme changes
npm install @lonik/themer
# or
yarn add @lonik/themer
# or
pnpm add @lonik/themer
# or
bun add @lonik/themerWrap your app with ThemeProvider in your root route:
import { ThemeProvider } from "@lonik/themer";
import { createRootRoute, Outlet } from "@tanstack/react-router";
export const Route = createRootRoute({
component: () => (
<ThemeProvider>
<Outlet />
</ThemeProvider>
),
});Use the useTheme hook to access and control the theme:
import { useTheme } from "@lonik/themer";
function ThemeSwitcher() {
const { theme, setTheme } = useTheme();
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme("light")}>Light</button>
<button onClick={() => setTheme("dark")}>Dark</button>
<button onClick={() => setTheme("system")}>System</button>
</div>
);
}By default, themes are applied via the class attribute on the <html> element:
/* CSS */
.light {
--background: white;
--text: black;
}
.dark {
--background: black;
--text: white;
}Or with Tailwind CSS using class variants:
<div className="bg-white dark:bg-black">Content</div>| Prop | Type | Default | Description |
|---|---|---|---|
themes |
string[] |
['light', 'dark'] |
List of available theme names |
defaultTheme |
string |
'system' (if enableSystem is true) or 'light' |
Default theme to use |
enableSystem |
boolean |
true |
Enable system theme detection |
enableColorScheme |
boolean |
true |
Apply color-scheme to html element |
storageKey |
string |
'theme' |
Key used to store theme in storage |
storage |
'localStorage' | 'sessionStorage' | 'cookie' | ThemeStorage |
'localStorage' |
Storage mechanism to persist theme |
attribute |
'class' | 'data-*' | Array |
'class' |
HTML attribute to apply theme (e.g., 'class', 'data-theme', ['class', 'data-mode']) |
value |
object |
undefined |
Mapping of theme names to attribute values |
forcedTheme |
string |
undefined |
Force a specific theme (ignores user preference) |
disableTransitionOnChange |
boolean |
false |
Disable CSS transitions when changing themes |
Returns an object with the following properties:
const {
theme, // Current theme name (e.g., 'light', 'dark', 'system')
setTheme, // Function to change theme
resolvedTheme, // Actual theme in use (resolves 'system' to 'light' or 'dark')
systemTheme, // System preference ('light' or 'dark')
themes, // Array of available themes
forcedTheme, // Forced theme if set
} = useTheme();// Direct value
setTheme("dark");
// Function form (for toggling)
setTheme((prev) => (prev === "dark" ? "light" : "dark"));Define your own theme names:
<ThemeProvider
themes={["light", "dark", "ocean", "forest", "sunset"]}
defaultTheme="ocean"
>
<App />
</ThemeProvider>.ocean {
--bg-primary: #001f3f;
--text-primary: #7fdbff;
}
.forest {
--bg-primary: #1a3d2e;
--text-primary: #90ee90;
}
.sunset {
--bg-primary: #ff6b35;
--text-primary: #ffe66d;
}Apply themes via data attributes instead of CSS classes:
<ThemeProvider attribute="data-theme">
<App />
</ThemeProvider>[data-theme="light"] {
/* styles */
}
[data-theme="dark"] {
/* styles */
}Apply themes to multiple attributes simultaneously:
<ThemeProvider attribute={["class", "data-mode"]}>
<App />
</ThemeProvider>This will apply both class="dark" and data-mode="dark" to the html element.
Map theme names to different attribute values:
<ThemeProvider
themes={["light", "dark"]}
value={{
light: "day",
dark: "night",
}}
>
<App />
</ThemeProvider>This will apply class="day" for light and class="night" for dark.
Persists across browser sessions:
<ThemeProvider storage="localStorage" storageKey="app-theme">
<App />
</ThemeProvider>Persists only for the current session:
<ThemeProvider storage="sessionStorage">
<App />
</ThemeProvider>Useful for SSR scenarios where you need access to theme on the server:
<ThemeProvider storage="cookie" storageKey="theme">
<App />
</ThemeProvider>Implement your own storage solution:
import { ThemeStorage } from '@lonik/themer'
const myStorage: ThemeStorage = {
getItem: (key) => {
// Your custom get logic
return customStore.get(key)
},
setItem: (key, value) => {
// Your custom set logic
customStore.set(key, value)
},
removeItem: (key) => {
// Optional: custom remove logic
customStore.remove(key)
},
subscribe: (key, callback) => {
// Optional: for cross-tab sync
const handler = (newValue) => callback(newValue)
customStore.on('change', handler)
return () => customStore.off('change', handler)
}
}
<ThemeProvider storage={myStorage}>
<App />
</ThemeProvider>Force a specific theme for a page or component (useful for landing pages or specific routes):
<ThemeProvider forcedTheme="dark">
<App />
</ThemeProvider>When forcedTheme is set, user preferences are ignored.
Prevent CSS transition flash when changing themes:
<ThemeProvider disableTransitionOnChange={true}>
<App />
</ThemeProvider>TanStack Themer automatically prevents flash of unstyled content (FOUC) by injecting an inline script that runs before React hydration. The script reads the stored theme and applies it to the DOM immediately.
Themer
The ThemeScript component (included in ThemeProvider) uses TanStack Router's ScriptOnce to inject the appropriate script based on your storage type:
localStorageScript- For localStoragesessionStorageScript- For sessionStoragecookieStorageScript- For cookie storage
This ensures the correct theme is applied before the page renders, preventing any flash of the wrong theme.
- User visits your site
- Inline script executes (before React hydration)
- Script reads theme from storage
- Script applies theme to
document.documentElement - React hydrates with correct theme already applied
- No flash of wrong theme!
Theme changes automatically sync across browser tabs. When you change the theme in one tab, all other tabs update immediately.
This works through the storage adapter's subscribe method, which listens for storage events:
// Automatically handled by the library
useEffect(() => {
return storage.subscribe?.("theme", (newValue) => {
setTheme(newValue ?? defaultTheme);
});
}, []);function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
{theme === "dark" ? "โ๏ธ" : "๐"}
</button>
);
}function ThemeSelector() {
const { theme, themes, setTheme } = useTheme();
return (
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
{themes.map((t) => (
<option key={t} value={t}>
{t.charAt(0).toUpperCase() + t.slice(1)}
</option>
))}
</select>
);
}To avoid hydration mismatches, wait for mount before rendering theme-dependent content:
import { useHydrated } from "@tanstack/react-router";
function ThemedComponent() {
const hydrated = useHydrated();
const { theme } = useTheme();
if (!hydrated) {
return <div>Loading...</div>;
}
return <div>Current theme: {theme}</div>;
}Use forcedTheme on specific routes:
// Dark theme for landing page
export const Route = createFileRoute("/landing")({
component: () => (
<ThemeProvider forcedTheme="dark">
<LandingPage />
</ThemeProvider>
),
});Note: Nested ThemeProvider components automatically pass through to the parent, so you can safely use multiple providers.
The library is written in TypeScript and exports all necessary types:
import type {
ThemeProviderProps,
UseThemeProps,
ThemeStorage,
BuiltInStorage,
Attribute,
} from "@lonik/themer";Works in all modern browsers that support:
matchMediaAPI for system theme detectionStorageAPI (localStorage/sessionStorage)- CSS custom properties (CSS variables)
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
MIT License - see LICENSE file for details
- Inspired by next-themes
- Built for TanStack Router