@lonik/themer
v0.5.2
Published
A starter for creating a React component library.
Maintainers
Readme
README generated by Claude Code, curated by @lukonik
Features
✓ 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
Installation
npm install @lonik/themer
# or
yarn add @lonik/themer
# or
pnpm add @lonik/themer
# or
bun add @lonik/themerQuick Start
Basic Setup
Wrap 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>
),
});Using the Theme
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>
);
}Styling with Themes
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>API Reference
ThemeProvider Props
| 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 |
useTheme Hook
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();setTheme
// Direct value
setTheme("dark");
// Function form (for toggling)
setTheme((prev) => (prev === "dark" ? "light" : "dark"));Advanced Usage
Custom Themes
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;
}Data Attribute
Apply themes via data attributes instead of CSS classes:
<ThemeProvider attribute="data-theme">
<App />
</ThemeProvider>[data-theme="light"] {
/* styles */
}
[data-theme="dark"] {
/* styles */
}Multiple Attributes
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.
Custom Value Mapping
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.
Storage Options
localStorage (default)
Persists across browser sessions:
<ThemeProvider storage="localStorage" storageKey="app-theme">
<App />
</ThemeProvider>sessionStorage
Persists only for the current session:
<ThemeProvider storage="sessionStorage">
<App />
</ThemeProvider>Cookie Storage
Useful for SSR scenarios where you need access to theme on the server:
<ThemeProvider storage="cookie" storageKey="theme">
<App />
</ThemeProvider>Custom Storage Adapter
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>Forced Theme
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.
Disable Transitions
Prevent CSS transition flash when changing themes:
<ThemeProvider disableTransitionOnChange={true}>
<App />
</ThemeProvider>SSR & FOUC Prevention
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.
How It Works
- 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!
Cross-Tab Synchronization
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);
});
}, []);Examples
Simple Theme Toggle
function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
{theme === "dark" ? "☀️" : "🌙"}
</button>
);
}Theme Selector Dropdown
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>
);
}Avoiding Hydration Mismatch
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>;
}Per-Route Themes
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.
TypeScript
The library is written in TypeScript and exports all necessary types:
import type {
ThemeProviderProps,
UseThemeProps,
ThemeStorage,
BuiltInStorage,
Attribute,
} from "@lonik/themer";Browser Support
Works in all modern browsers that support:
matchMediaAPI for system theme detectionStorageAPI (localStorage/sessionStorage)- CSS custom properties (CSS variables)
Contributing
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
License
MIT License - see LICENSE file for details
Acknowledgments
- Inspired by next-themes
- Built for TanStack Router
