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

ssr-themes

v0.3.4

Published

Theming is hard with SSR.

Readme

ssr-themes Version

Theming is hard with SSR.

The server often isn't aware of the client theme; most libraries keep it in local storage. The skew between the server default and what is hydrated on the client often results in a flash of the wrong content.

ssr-themes keeps the SSR markup, bootstrap script, and hydrated app in sync. It uses cookies to store the selected theme + system default and has first-party bindings for React, Svelte, Vue, and more. This means:

  • 🍪 Perfect cookie-driven SSR markup
  • ✨ No client/server mismatch
  • 🌓 System theme support
  • 🔄 Built-in cross-tab sync
  • 🛡️ Strongly typed bindings

Demo of ssr-themes not flashing vs next-themes

Check out the main demo: https://ssr-themes.cadams.io/.

Or browse the framework examples:

| Framework | Live demo | Source | | -------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | Next.js | next.ssr-themes.cadams.io | examples/next | | React Router | react-router.ssr-themes.cadams.io | examples/react-router | | TanStack Start | start.ssr-themes.cadams.io | examples/tanstack-start | | SolidStart | solid.ssr-themes.cadams.io | examples/solid | | SvelteKit | svelte.ssr-themes.cadams.io | examples/svelte | | Vue / Nuxt | vue.ssr-themes.cadams.io | examples/nuxt | | Astro | astro.ssr-themes.cadams.io | examples/astro |

Install

bun add ssr-themes
# or
pnpm add ssr-themes
# or
npm install ssr-themes
# or
yarn add ssr-themes

How It Works

ssr-themes has three parts:

  1. parseThemeCookie() and registerTheme() help the server pre-render the correct theme during SSR. This is optional.
  2. themeScript() runs before hydration on the client and makes sure the theme on <html> is set to the correct value (and fills in the value from the client if it's system).
  3. ThemeProvider keeps the DOM, the theme cookie, and client state in sync after mount.
import {createTheme} from 'ssr-themes';
import {bindTheme} from 'ssr-themes/react';

const {
  options,
  registerTheme,
  parseThemeCookie,
  themeScript,
} = createTheme();

const {ThemeProvider} = bindTheme(options);

const App = () => {
  // Remove `parseThemeCookie()` and `registerTheme()` to drive theme by client script only
  // Similar to `next-themes`, this will then trigger server/client mismatch in React
  // and you should pass `suppressHydrationWarning`
  const initial = parseThemeCookie(cookieHeader);

  return (
    <html {...registerTheme(initial)}>
      <head>
        <script id="ssr-themes">
          {themeScript()}
        </script>
      </head>
      <body>
        <ThemeProvider initial={initial}>
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
};

Why not next-themes?

next-themes is popular because it makes client-side theming in React and Next.js easy.

But it solves a subset of the problem.

Its docs explicitly warn that reading theme before mount is hydration-unsafe, because the server does not know the current theme yet. That is a reasonable tradeoff if all you need is client-resolved theme state.

ssr-themes is for apps that want the theme to participate on the server. This means you can:

  • Read the theme and system preference from the request cookie during SSR
  • Pre-render the 100% correct HTML on the server (<select>, etc.)

But you don't even need to use the SSR helpers. They are optional if/when you need to render conditional UI based on the theme. You'll probably do this with a theme picker.

If you only need client-side theme state in a Next.js app, next-themes is a good fit.

If your SSR markup depends on the theme (or if you don't use Next.js), ssr-themes is a good fit.

Check out the Next.js example for a cache-friendly App Router setup. It uses proxy.ts plus listVariants() so the public / route stays cacheable and layouts do not read cookies.

Styling

Class-Based Theming

By default, ssr-themes writes a class to <html>.

:root {
  --background: white;
  --foreground: black;
}

:root.dark {
  --background: black;
  --foreground: white;
}

If you prefer data-* attributes, set attribute accordingly.

Tailwind CSS

All examples in this repo use Tailwind v4 with class-based dark mode - feel free to check them out for more detail:

@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));

API

The API has two entrypoints:

  • createTheme() from ssr-themes
  • bindTheme() from ssr-themes/react, ssr-themes/solid, ssr-themes/vue, or ssr-themes/svelte

The core theme state uses three fields:

  • selected: the saved theme preference
  • resolved: the concrete theme applied to the document
  • system: the browser's current light/dark preference

createTheme()

Use createTheme() once to define the shared theme config for your app.

import {createTheme} from 'ssr-themes';

const theme = createTheme({
  themes: ['light', 'dark', 'quartz'],
  defaultTheme: 'system',
  attribute: ['class', 'data-theme'],
  valueMap: {
    light: 'day',
    dark: 'night',
    quartz: 'quartz',
  },
  cookie: {
    name: 'theme',
    secure: true,
    sameSite: 'lax',
  },
});

createTheme() accepts stable app-level config such as:

  • themes - the allowed theme names
  • defaultTheme - the fallback selected theme when no saved preference exists
  • enableSystem - whether 'system' is a selectable theme
  • enableColorScheme - whether to set CSS color-scheme for resolved 'light' and 'dark'
  • attribute - where the active theme is written on <html>, such as 'class', 'data-theme', or both
  • valueMap - maps theme names to the DOM values written to those attributes
  • cookie - how the selected theme is persisted, including name, path, maxAge, expires, sameSite, domain, and secure

It returns:

  • options - the exact config object passed to createTheme()
  • defaultVariant - the fallback serialized variant for this config
  • encodeVariant() - serializes theme state into a stable variant string
  • decodeVariant() - decodes a variant string back into resolved theme state
  • listVariants() - returns the finite set of valid pre-renderable variants
  • parseThemeCookie() - reads theme state from a raw Cookie header
  • registerTheme() - returns SSR attributes for <html>
  • themeScript() - returns the inline bootstrap script that applies the theme before hydration

bindTheme()

Use bindTheme() in the framework entrypoint for your app.

import {bindTheme} from 'ssr-themes/react';

const {ThemeProvider, useTheme} = bindTheme(theme);
// or: bindTheme(theme.options)

bindTheme() accepts either:

  • the full object returned by createTheme()
  • theme.options

It returns:

  • ThemeProvider
  • useTheme()

ThemeProvider only takes runtime props:

  • initial - SSR theme state to reuse during hydration
  • forced - force a concrete theme for the current render or page
  • disableTransition - disable CSS transitions while the theme changes
  • nonce - CSP nonce for inline style elements

useTheme() must be used within ThemeProvider.

All bindings expose the same conceptual state:

  • themes
  • selected
  • setSelected(next)
  • forced
  • resolved
  • system

The exact shape is framework-native:

  • React returns plain values
  • Solid returns accessors
  • Vue returns refs and computed values
  • Svelte returns stores

parseThemeCookie()

Use parseThemeCookie() to read the saved theme from a raw Cookie header.

const initial = theme.parseThemeCookie(cookieHeader);

It reads the cookie configured by createTheme() and returns resolved theme state for the current request.

It returns undefined when the cookie is:

  • missing
  • empty
  • malformed
  • not in the allowed theme list

When present, the return value includes:

  • selected
  • resolved
  • system

registerTheme()

Use registerTheme() to pre-render the current theme on <html> during SSR.

<html {...theme.registerTheme(initial)} />

It usually receives the result of parseThemeCookie() and returns one of three output shapes depending on renderMode:

  • jsx (default) -> {className, style, ...dataAttrs}
  • html-attrs -> {class, style, ...dataAttrs}
  • html-string -> class="..." style="..." data-theme="..."

Examples:

const jsxProps = theme.registerTheme(initial);

const htmlAttrs = theme.registerTheme(initial, {
  renderMode: 'html-attrs',
});

const htmlString = theme.registerTheme(initial, {
  renderMode: 'html-string',
});

Runtime overrides are:

  • forced
  • renderMode
  • className
  • style

Notes:

  • it applies your configured attribute and valueMap
  • it adds color-scheme automatically for resolved 'light' and 'dark' unless disabled
  • in JSX mode, it may add suppressHydrationWarning when SSR cannot fully resolve the theme

themeScript()

Use themeScript() to generate the inline bootstrap script that runs on the client before hydration.

<script id="ssr-themes">{theme.themeScript()}</script>
<script id="ssr-themes">
  {theme.themeScript({forced: 'dark'})}
</script>

themeScript():

  • reads the saved theme from the cookie
  • resolves 'system' on the client when needed
  • updates the <html> attributes
  • sets color-scheme when appropriate

themeScript() only supports one runtime override:

  • forced

Render it near the top of the document so the correct theme is applied before the app hydrates.

Variants: defaultVariant, encodeVariant(), decodeVariant(), listVariants()

These helpers are for routing, caching, and pre-rendering.

const variant =
  theme.encodeVariant(
    theme.parseThemeCookie(cookieHeader),
  ) ?? theme.defaultVariant;

const initial = theme.decodeVariant(variant);
const variants = theme.listVariants();
  • defaultVariant is the fallback serialized variant for the current config
  • encodeVariant() returns a stable string when there is enough theme state to serialize
  • decodeVariant() returns resolved theme state, or undefined for invalid values
  • listVariants() returns the full set of valid pre-renderable theme variants

Encoded variants always include the system hint:

  • explicit themes: light~l, light~d, dark~l, dark~d
  • system mode: ~l, ~d

Skills

The NPM package ships with AI agent skills to help migration from next-themes to ssr-themes.