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

@sjohansson/astro-theme-toggle

v0.3.1

Published

Framework-agnostic theme toggle with Web Components, optional Astro wrappers

Downloads

686

Readme

Astro Theme Toggle

A comprehensive, SSR-friendly theme system built for Astro 6+. Supports multiple theme modes including light, dark, high-contrast variants, and system preference detection. Works with Tailwind CSS 4 or plain CSS and persists user preferences using localStorage.

Features

  • 🎨 5 Theme Modes: System, Light, Dark, High Contrast Light, High Contrast Dark
  • Accessible: Full keyboard navigation and ARIA support
  • 🎯 Type-Safe: Full TypeScript support with exported types
  • 🎨 CSS Custom Properties: Easy theming with CSS variables
  • 📦 Lightweight: No external dependencies
  • 🔄 View Transitions: Works seamlessly with Astro view transitions
  • 🎨 Theme Preview: Built-in component to visualize all color tokens
  • 🛠️ Customizable: Define your own theme configurations

Installation

pnpm add @sjohansson/astro-theme-toggle

Components

ThemeController (Advanced)

The enhanced theme controller with support for multiple theme modes and system preference detection.

Basic Usage

---
import { ThemeController } from '@sjohansson/astro-theme-toggle';
---

<!-- Single icon button, expands on click -->
<ThemeController />

Expand Direction

The component starts as a single icon button showing the current theme. On click it expands to reveal all theme options as icon buttons.

<!-- Auto (default): vertical on desktop, horizontal on mobile -->
<ThemeController expandDirection="auto" />

<!-- Always expand vertically (column of buttons above/below) -->
<ThemeController expandDirection="vertical" />

<!-- Always expand horizontally (row of buttons to the side) -->
<ThemeController expandDirection="horizontal" />

<!-- With labels visible when expanded -->
<ThemeController expandDirection="vertical" showLabels={true} />

Props

| Prop | Type | Description | Default | |------|------|-------------|---------| | class | string | Optional CSS classes | "" | | expandDirection | "horizontal" \| "vertical" \| "auto" | Direction the options panel expands | "auto" | | sectionsDirection | "horizontal" \| "vertical" \| "auto" | How the Theme/Scheme/Contrast/Color vision sections are arranged | "auto" | | expandSide | "auto" \| "start" \| "end" | Which side of the trigger the panel opens toward | "auto" | | showLabels | boolean | Show theme labels next to icons | false | | labelPosition | "auto" \| "below" \| "above" \| "right" \| "left" | Where labels sit relative to icons | "auto" | | preset | "basic" \| "accessible" \| "full" \| ThemeAxis[] | Which axis controls to offer (scheme/contrast/variation; legacy ThemeCategory[] values are mapped) | "basic" | | family | string | Restrict to a single family by id (variant-only UI) | — | | themes | ThemeConfig[] | Custom theme configurations | Default themes | | applyMode | "inline" \| "attribute" \| "both" | How the active theme is reflected on <html> | "inline" | | attributeName | string | Base data-attribute name (coerced to data-…) | "data-theme" | | attributeCompanions | boolean | Also set -family/-scheme/-contrast/-variation companions | true |

Data-attribute theming

By default the controller applies the active theme as inline CSS custom properties on <html> (applyMode="inline"). Set applyMode="attribute" (or "both") to instead/also reflect the theme as data attributes, so you can drive all styling from your own CSS:

<ThemeController applyMode="attribute" />

This sets, on <html>, one attribute per axis:

<html
  data-theme="high-contrast-dark"
  data-theme-family="default"
  data-theme-scheme="dark"
  data-theme-contrast="more"
>

Then style with whatever granularity you need:

/* exact theme */
[data-theme="high-contrast-dark"] { --my-bg: #000; }

/* any dark scheme */
[data-theme-scheme="dark"] { color-scheme: dark; }

/* any high-contrast theme */
[data-theme-contrast="more"] { --border-width: 2px; }

/* a whole family */
[data-theme-family="kawaii"] { --accent: hotpink; }

Three orthogonal axes

Theme selection is modelled as three independent axes, each its own attribute, control, and ThemeConfig facet:

| Axis | Attribute | ThemeConfig field | Values | | --- | --- | --- | --- | | scheme | data-theme-scheme | scheme | light / dark | | contrast | data-theme-contrast | contrast | normal / more | | color-vision | data-theme-variation | variation | normal + e.g. protanopia |

Because they're independent, a theme (and the controller) can combine them — e.g. high contrast and protanopia at the same time, which a single category could never express. In attribute mode the attributes simply compose in your CSS, so the combination needs no hand-authored palette:

<html data-theme-scheme="light" data-theme-contrast="more" data-theme-variation="protanopia">
[data-theme-variation="protanopia"] { --accent: #6a5acd; }
[data-theme-contrast="more"][data-theme-variation="protanopia"] {
  /* high-contrast tuning for protanopia, composed from two axes */
}

A ThemeConfig opts into the non-default end of an axis with the optional contrast / variation fields (omit them for normal contrast / normal vision):

{ id: "seventies-protanopia-light", variation: "protanopia", scheme: "light", /* … */ }

Presets gate which axis controls appear (basic = scheme; accessible =

  • contrast; full = + color-vision), and each variation is offered as its own option — no collapsing. New color-vision types scale in just by adding themes with a new variation; no preset changes needed.

Inline-mode note: in inline/both mode the component resolves to the nearest authored palette and falls back (relaxing color-vision, then contrast) when a combination has no concrete theme. Attribute mode has no such limit — the axis attributes always reflect your exact selection.

Clean slate: in attribute mode the component sets only the attributes — it does not inject any colors. You author the [data-theme=…] rules. (In both mode the inline --theme-* variables are also set and take precedence.)

The theme-* / scheme-* / family-* classes are always set regardless of mode, so existing class-based CSS keeps working.

Customizing the attribute

<!-- Custom base name; companions derive from it (data-color-mode-scheme, …) -->
<ThemeController applyMode="attribute" attributeName="data-color-mode" />

<!-- Single combined attribute only, no companions -->
<ThemeController applyMode="attribute" attributeCompanions={false} />

A name not starting with data- is coerced (modedata-mode).

Generating a starter stylesheet

If you'd rather not hand-write every rule, generateThemeStylesheet turns a set of theme configs into [data-theme="id"] { …vars… } blocks (it is not injected automatically):

import { defaultThemes, generateThemeStylesheet } from '@sjohansson/astro-theme-toggle';

const css = generateThemeStylesheet(defaultThemes);            // base name 'data-theme'
const custom = generateThemeStylesheet(defaultThemes, 'data-color-mode');

ThemeToggle (Simple)

The original simple light/dark toggle for basic use cases. By default it toggles the .dark class on <html>. It supports the same data-attribute opt-in via apply-mode:

<!-- Default: toggles `.dark` -->
<ThemeToggle />

<!-- Sets data-theme="light|dark" (+ data-theme-scheme), no `.dark` class -->
<theme-toggle apply-mode="attribute"></theme-toggle>

<!-- Both: `.dark` class AND data attribute -->
<theme-toggle apply-mode="both" attribute-name="data-mode"></theme-toggle>

| Attribute | Values | Default | |------|------|---------| | apply-mode | class | attribute | both | class | | attribute-name | string (coerced to data-…) | data-theme | | attribute-companions | true | false (sets -scheme) | true |

As an Astro Integration (Recommended)

Use the integration for automatic setup and configuration:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import themeToggle from '@sjohansson/astro-theme-toggle/integration';

export default defineConfig({
  integrations: [
    themeToggle({
      // Optional: inject theme initialization script globally
      injectScript: true,
      // Optional: when using data-attribute theming, make the injected FOUC
      // script replay the attribute(s) before first paint. Match these to the
      // component's `applyMode` / `attributeName` / `attributeCompanions`.
      applyMode: 'attribute',
      attributeName: 'data-theme',
      attributeCompanions: true,
    })
  ],
});

Then use the component in your pages:

---
import { ThemeToggle } from '@sjohansson/astro-theme-toggle';
---

<ThemeToggle
  class="rounded-md border border-surface-3 bg-surface-1 px-3 py-2 text-surface-12 hover:bg-surface-2"
/>

Benefits of using the integration:

  • Automatic theme initialization script (eliminates FOUC)
  • Better SSR handling
  • Configuration validation

As a Standalone Component

For minimal setup, import and use directly:

---
import { ThemeToggle } from '@sjohansson/astro-theme-toggle';
---

<ThemeToggle
  class="rounded-md border border-surface-3 bg-surface-1 px-3 py-2"
/>

Props

| Prop | Type | Description | Default | |------|------|-------------|---------| | class | string | Optional CSS classes | "" |

ThemePreview

A documentation component that displays all theme colors and tokens.

---
import { ThemePreview } from '@sjohansson/astro-theme-toggle';
---

<!-- Show all themes -->
<ThemePreview />

<!-- Show specific theme -->
<ThemePreview theme="dark" />

Props

| Prop | Type | Description | Default | |------|------|-------------|---------| | class | string | Optional CSS classes | "" | | theme | "all" \| ThemeMode | Theme(s) to display | "all" | | themes | ThemeConfig[] | Custom theme configurations | Default themes |

API

Integration Options

| Option | Type | Description | Default | | --------------------- | ------------------------------------- | ------------------------------------------------------------- | ------------- | | injectScript | boolean | Inject theme initialization script in head | false | | applyMode | "inline" \| "attribute" \| "both" | When attribute/both, the injected script replays the theme data attribute(s) before paint | "inline" | | attributeName | string | Base data-attribute name the script replays (match the component) | "data-theme" | | attributeCompanions | boolean | Whether the script sets companion attributes on first visit | true |

Preventing FOUC with data attributes

The integration's injectScript injects a tiny inline <head> script. With applyMode: 'attribute' (or 'both') it replays the data attribute(s) before the page paints. The same script is available directly:

import { generateThemeInitScript, themeInitScript } from '@sjohansson/astro-theme-toggle';

// scheme-class only (default, backwards compatible)
const basic = themeInitScript;

// also replay data attributes (attribute / both apply modes)
const withAttrs = generateThemeInitScript({
  applyAttribute: true,
  attributeName: 'data-theme',
  companions: true,
});

The components persist the last resolved selection to localStorage so the script can replay it without bundling the theme list. Keys written in attribute/both mode: theme-attr-name, theme-resolved-id, theme-resolved-family, theme-resolved-scheme, theme-resolved-contrast, theme-resolved-variation, theme-attr-companions. The controller also stores the raw axis selections theme-family / theme-scheme / theme-contrast / theme-variation, plus a theme-mode mirror of the scheme axis for the base script and <theme-toggle> interop.

System-mode caveat: when an axis is following the OS (scheme or contrast set to system), the script recomputes that axis live from matchMedia, so scheme/contrast-based CSS is always correct on first paint. The combined data-theme id may be one frame stale across an OS preference change made while the page was closed; the component corrects it as soon as it hydrates.

Component Props

Using CSS Custom Properties

The theme system exposes CSS custom properties that you can use in your styles:

Styling

.my-component {
  /* Backgrounds */
  background: var(--theme-bg-primary);
  
  /* Text colors */
  color: var(--theme-fg-primary);
  
  /* Borders */
  border: 1px solid var(--theme-border-default);
  
  /* Interactive elements */
  button {
    background: var(--theme-interactive-default);
  }
  
  button:hover {
    background: var(--theme-interactive-hover);
  }
  
  /* Semantic colors */
  .success {
    color: var(--theme-success);
  }
}

Available CSS Variables

Background Colors

  • --theme-bg-primary - Primary background
  • --theme-bg-secondary - Secondary background
  • --theme-bg-tertiary - Tertiary background

Foreground Colors

  • --theme-fg-primary - Primary text
  • --theme-fg-secondary - Secondary text
  • --theme-fg-tertiary - Tertiary text

Border Colors

  • --theme-border-default - Default border
  • --theme-border-hover - Hover state border
  • --theme-border-focus - Focus state border

Interactive Colors

  • --theme-interactive-default - Default interactive elements
  • --theme-interactive-hover - Hover state
  • --theme-interactive-active - Active state
  • --theme-interactive-disabled - Disabled state

Semantic Colors

  • --theme-success - Success messages
  • --theme-warning - Warning messages
  • --theme-error - Error messages
  • --theme-info - Info messages

Sizing

  • --theme-trigger-size - Width/height of the trigger button (default 2.5rem)
  • --theme-trigger-icon-size - Trigger icon size (default: half of --theme-trigger-size, i.e. 1.25rem). Set the trigger size alone and the icon scales with it, staying centred; override this to tune the icon independently.
/* Shrink the whole trigger — the icon scales and stays centred automatically */
.theme-trigger {
  --theme-trigger-size: 1.75rem;
}

Custom Theme Configuration

You can define your own themes by creating a custom configuration:

---
import { ThemeController, type ThemeConfig } from '@sjohansson/astro-theme-toggle';

const customThemes: ThemeConfig[] = [
  {
    mode: "light",
    label: "Light Mode",
    description: "My custom light theme",
    colors: {
      background: {
        primary: "#ffffff",
        secondary: "#f5f5f5",
        tertiary: "#e0e0e0",
      },
      foreground: {
        primary: "#000000",
        secondary: "#333333",
        tertiary: "#666666",
      },
      border: {
        default: "#cccccc",
        hover: "#999999",
        focus: "#0066cc",
      },
      interactive: {
        default: "#0066cc",
        hover: "#0052a3",
        active: "#003d7a",
        disabled: "#999999",
      },
      semantic: {
        success: "#00aa00",
        warning: "#ff8800",
        error: "#cc0000",
        info: "#0088cc",
      },
    },
  },
  // Add more themes...
];
---

<ThemeController themes={customThemes} />

TypeScript Support

The package exports TypeScript types for full type safety:

import type { 
  ThemeMode,
  ThemeConfig,
  ThemeControllerProps,
  ColorToken 
} from '@sjohansson/astro-theme-toggle';

Accessibility

  • Full keyboard navigation support
  • ARIA labels and roles
  • Focus indicators
  • Screen reader friendly
  • Respects prefers-color-scheme and prefers-contrast media queries

Styling

Tailwind CSS 4

Works seamlessly with Tailwind CSS 4 utility classes:

<ThemeController 
  class="rounded-lg shadow-lg border-2"
/>

Custom CSS

Target the built-in selectors for custom styling:

.theme-controller {
  /* Container */
}

.theme-trigger {
  /* The single icon button */
}

.theme-panel {
  /* The expandable options panel */
}

.theme-option-btn {
  /* Individual theme option buttons */
}

Development

pnpm dev        # Rebuild on changes
pnpm test       # Run package tests
pnpm build      # Emit ESM, types, and copy .astro assets to dist/
pnpm clean      # Remove build output

This package is designed to stay self-contained and publish-ready—no cross-package dependencies are required.

Learn More