npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lonik/themer

v0.5.2

Published

A starter for creating a React component library.

Readme

NPM Version License

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/themer

Quick 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 localStorage
  • sessionStorageScript - For sessionStorage
  • cookieStorageScript - For cookie storage

This ensures the correct theme is applied before the page renders, preventing any flash of the wrong theme.

How It Works

  1. User visits your site
  2. Inline script executes (before React hydration)
  3. Script reads theme from storage
  4. Script applies theme to document.documentElement
  5. React hydrates with correct theme already applied
  6. 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:

  • matchMedia API for system theme detection
  • Storage API (localStorage/sessionStorage)
  • CSS custom properties (CSS variables)

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

MIT License - see LICENSE file for details

Acknowledgments

Links