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

@maholan/theme

v0.2.1

Published

Theme provider and dark mode management for the MHL Untitled UI Platform.

Downloads

235

Readme

@maholan/theme

Theme provider and dark mode management for the MHL Untitled UI Platform.

Features

  • Light/Dark mode — explicit toggle or system-preference tracking
  • Flash-free SSR<ThemeScript> Server Component eliminates flash of wrong theme
  • Persistent — user choice survives page refreshes via localStorage
  • Stable toggleMode — stable callback reference; never recreates on re-render
  • Transition suppression — optional disableTransitionOnChange prevents color-flicker
  • forcedMode — pin a subtree to a specific mode (useful for Storybook previews)
  • onModeChange callback — hook into mode changes for analytics or state sync
  • CSP-readynonce prop on both ThemeScript and ThemeProvider
  • Type-safe — full TypeScript with strict mode

Installation

pnpm add @maholan/theme @maholan/tokens
# peer dependencies
pnpm add react react-dom

Quick Start (Next.js App Router)

// app/layout.tsx
import { ThemeProvider, ThemeScript } from "@maholan/theme";
import "./globals.css";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    // suppressHydrationWarning is REQUIRED — ThemeScript writes the class before
    // React hydrates, so the server-rendered attribute may differ from the client.
    <html lang="en" suppressHydrationWarning>
      <body>
        {/*
          ThemeScript MUST be the first child of <body>.
          It runs synchronously before the first paint — zero content flash.
          Options must mirror ThemeProvider exactly.
        */}
        <ThemeScript defaultMode="light" storageKey="mhl-theme-mode" />
        <ThemeProvider
          defaultMode="light"
          storageKey="mhl-theme-mode"
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

Toggle in a client component

"use client";

import { useTheme } from "@maholan/theme";

export function ThemeToggle() {
  const { resolvedMode, toggleMode } = useTheme();

  return (
    <button onClick={toggleMode} aria-label="Toggle theme">
      {resolvedMode === "light" ? "🌙 Dark" : "☀️ Light"}
    </button>
  );
}

API Reference

<ThemeScript> — Server Component

Renders a tiny (~250 byte) blocking inline <script> that applies the correct mode to <html> before the first paint. Has no runtime cost after load.

Place it as the first child of <body> and make sure <html> has suppressHydrationWarning.

All props must match the corresponding <ThemeProvider> props exactly.

| Prop | Type | Default | Description | | ------------------------- | ------------------------- | ------------------ | -------------------------------------------------------- | | defaultMode | 'light' \| 'dark' | 'light' | Fallback when no stored preference exists | | defaultSystemPreference | boolean | false | Fall back to OS preference instead of defaultMode | | storageKey | string | 'mhl-theme-mode' | localStorage key to read the persisted preference from | | attribute | 'class' \| 'data-theme' | 'class' | DOM attribute to write on <html> | | forcedMode | 'light' \| 'dark' | undefined | Ignore all preferences — always use this mode | | nonce | string | undefined | CSP nonce for the inline <script> tag |

Mode resolution priority

  1. forcedMode (if set)
  2. Stored localStorage value
  3. OS prefers-color-scheme (only when defaultSystemPreference={true})
  4. defaultMode

<ThemeProvider> — Client Component

Provides theme context to your React tree. Must be a client component boundary (it adds "use client" internally).

| Prop | Type | Default | Description | | --------------------------- | --------------------------- | ------------------ | --------------------------------------------------------------------- | | children | ReactNode | Required | Application subtree | | defaultMode | 'light' \| 'dark' | 'light' | Mode when no stored preference exists and OS tracking is off | | defaultSystemPreference | boolean | false | Follow OS preference on first load (when no stored preference exists) | | storageKey | string | 'mhl-theme-mode' | localStorage key for persisting the preference | | attribute | 'class' \| 'data-theme' | 'class' | DOM attribute written on <html> | | forcedMode | 'light' \| 'dark' | undefined | Pin to a specific mode; disables all toggling | | disableTransitionOnChange | boolean | false | Inject * { transition: none } during mode switch to prevent flicker | | onModeChange | (mode: ThemeMode) => void | undefined | Callback fired after the DOM is updated with the new mode | | nonce | string | undefined | CSP nonce forwarded to injected <style> tags |

defaultSystemPreference is false by default. This ensures defaultMode is always respected. Set it to true only if you want the theme to follow the user's OS setting when they have no stored preference.


useTheme() — Hook

Access theme context from any client component inside <ThemeProvider>.

import { useTheme } from "@maholan/theme";

const {
  mode, // ThemeMode — the user's chosen mode
  resolvedMode, // ThemeMode — the mode actually applied to the DOM
  //   equals forcedMode when forced; otherwise equals mode
  systemPreference, // boolean  — whether OS tracking is active
  setMode, // (mode: ThemeMode) => void
  toggleMode, // () => void — stable reference, safe in dependency arrays
  setSystemPreference, // (enabled: boolean) => void
} = useTheme();

mode vs resolvedMode

| Value | When to use | | -------------- | ------------------------------------------------------ | | mode | Display what the user has chosen in settings UI | | resolvedMode | Determine what is visually active (e.g., icon to show) |


Examples

System Preference Settings Panel

"use client";

import { useTheme } from "@maholan/theme";

export function ThemeSettings() {
  const { mode, resolvedMode, setMode, systemPreference, setSystemPreference } =
    useTheme();

  return (
    <div className="space-y-4">
      <h2>Appearance</h2>

      <label className="flex items-center gap-2">
        <input
          type="checkbox"
          checked={systemPreference}
          onChange={(e) => setSystemPreference(e.target.checked)}
        />
        Match system appearance
      </label>

      {!systemPreference && (
        <div className="flex gap-2">
          <button
            onClick={() => setMode("light")}
            aria-pressed={mode === "light"}
          >
            Light
          </button>
          <button
            onClick={() => setMode("dark")}
            aria-pressed={mode === "dark"}
          >
            Dark
          </button>
        </div>
      )}

      <p className="text-sm text-gray-500">
        Currently displaying: <strong>{resolvedMode}</strong>
      </p>
    </div>
  );
}

Forced Mode (Storybook / Docs Previews)

// Force dark mode for a specific story or preview pane
<ThemeProvider forcedMode="dark">
  <ComponentPreview />
</ThemeProvider>

Analytics / State Sync

<ThemeProvider
  defaultMode="light"
  onModeChange={(mode) => {
    analytics.track("theme_changed", { mode });
    store.dispatch(setTheme(mode));
  }}
>
  {children}
</ThemeProvider>

CSP with Nonce

// Retrieve nonce from your CSP middleware (Next.js example)
import { headers } from "next/headers";

export default async function RootLayout({ children }) {
  const nonce = (await headers()).get("x-nonce") ?? undefined;

  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeScript defaultMode="light" nonce={nonce} />
        <ThemeProvider defaultMode="light" nonce={nonce}>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}

data-theme Attribute (instead of class)

<ThemeScript attribute="data-theme" defaultMode="light" />
<ThemeProvider attribute="data-theme" defaultMode="light">
  {children}
</ThemeProvider>
/* Target with CSS */
:root {
  --bg: #fff;
  --fg: #111;
}
[data-theme="dark"] {
  --bg: #111;
  --fg: #fff;
}

How It Works

Zero-Flash Architecture

Browser request
      │
      ▼
Next.js renders HTML (SSR)
  └─ ThemeScript injects inline <script> first in <body>
        │
        ▼
Browser parses HTML, executes <script> synchronously
  └─ Reads localStorage → applies "light" or "dark" to <html>
        │
        ▼
Browser paints first frame (correct theme, no flash)
        │
        ▼
React hydrates → ThemeProvider reads same localStorage value
  └─ suppressHydrationWarning prevents mismatch warnings

Theme Application on the DOM

<!-- attribute="class" (default) -->
<html class="dark">
  <!-- attribute="data-theme" -->
  <html data-theme="dark"></html>
</html>

Mode Resolution Priority

Both ThemeScript and ThemeProvider resolve the initial mode using the same priority chain so they always agree:

  1. forcedMode — hard override
  2. Stored localStorage[storageKey] — explicit user choice
  3. OS prefers-color-scheme — only when defaultSystemPreference={true}
  4. defaultMode — final fallback

System Preference Tracking

When systemPreference is true, the provider subscribes to window.matchMedia('(prefers-color-scheme: dark)') and updates the theme whenever the OS setting changes.

Calling setSystemPreference(true) clears the stored preference so the OS value is in full control. Calling setSystemPreference(false) persists the current mode to localStorage.


Tailwind CSS v4 Integration

/* globals.css */
@import "tailwindcss";

/* Register dark variant — applies when <html> has class="dark" */
@custom-variant dark (&:where(.dark, .dark *));

@layer base {
  body {
    background-color: var(--color-bg-primary);
    color: var(--color-text-primary);
  }
}
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">
  Hello, world
</div>

For Tailwind v3, set darkMode: ['class'] in tailwind.config.ts.


Troubleshooting

Still seeing a theme flash

Ensure <ThemeScript> is the first child of <body>, not in <head> and not after any other elements.

Hydration mismatch warnings

Add suppressHydrationWarning to the <html> element (not <body>).

Wrong theme on first load

Verify that storageKey, attribute, defaultMode, and defaultSystemPreference are identical on both <ThemeScript> and <ThemeProvider>.

Theme not persisting in incognito

localStorage is unavailable in some private-browsing contexts. The provider falls back gracefully to defaultMode.

useTheme throws outside provider

useTheme must be called inside a component that is a descendant of <ThemeProvider>. If you call it at the top level of a layout before the provider is mounted, wrap it in a client component that lives below the provider.


Exports

// Components
export { ThemeProvider } from "@maholan/theme"; // "use client" runtime context
export { ThemeScript } from "@maholan/theme"; // Server Component — SSR flash prevention

// Hook
export { useTheme } from "@maholan/theme"; // Read theme context in client components

// Types
export type {
  ThemeMode,
  ThemeContextValue,
  ThemeProviderProps,
  ThemeScriptProps,
} from "@maholan/theme";

// Build-time CSS generation (advanced)
export { generateGlobalCss, generateCssVarsForMode } from "@maholan/theme";
export { hslToHex, isHslFormat } from "@maholan/theme";

Related Packages

License

MIT