omni-themes

Universal theme library that works with any JavaScript framework. Provides perfect dark mode, system theme detection, and no-flash loading.

View on GitHub

Omni-Themes

A universal theme library that works across all JavaScript frameworks, inspired by next-themes but framework-agnostic. Omni-Themes provides seamless theme management with support for multiple custom themes, system preference detection, and cross-tab synchronization.

Features:

Installation

npm install @madooei/omni-themes

Usage

A universal theme library that works across all JavaScript frameworks, inspired by next-themes but framework-agnostic. Omni-Themes provides seamless theme management with support for multiple custom themes, system preference detection, and cross-tab synchronization.

Basic Setup

import { createThemeStore } from "@madooei/omni-themes";

// Create theme store
const {
  themes, // Available themes array
  $theme, // Current theme atom
  $resolvedTheme, // Resolved theme atom (handles 'system')
  $systemTheme, // System preference atom
  setTheme, // Function to change theme
  applyThemeScriptString, // Script for FOUC prevention
} = createThemeStore({}); // Default options

// Listen for theme changes
$theme.listen((theme) => {
  console.log("Theme changed to:", theme);
});

// Change theme
setTheme("dark");

The variables which their names start with $ are Nanostores atoms.

[!NOTE] An atom is a reactive variable that can be listened to for changes.

For example, $theme is an atom that provides the following operations:

[!NOTE]
subscribe calls the callback immediately with the store’s current value and then on every future change, while listen only calls the callback when the value changes (not at subscription time). Use subscribe when you want the current state right away; use listen to react only to updates.

Nanostores atoms also support set(value: T) to update the value. However, in Omni-Themes, you should use the setTheme function to change the theme, as it handles additional logic like system preference detection, etc. To get technical, we expose ReadableAtom for read-only access to the theme related atoms so you cannot access set directly on them.

Let’s explore the variables returned by createThemeStore in more detail:

Multiple Themes (Vanilla JavaScript Example)

Here is a more complete example that shows how to set up multiple themes and use the setTheme function to switch between them:

import { createThemeStore } from "@madooei/omni-themes";

// Create a theme store with multiple themes for vanilla JS demo
export const {
  themes,
  $theme,
  $resolvedTheme,
  $systemTheme,
  setTheme,
  applyThemeScriptString,
  createForcedThemeScriptString,
} = createThemeStore({
  themes: ["light", "dark", "blue", "green", "purple", "ocean"],
  defaultTheme: "light",
  defaultLightTheme: "light",
  defaultDarkTheme: "dark",
  themesMap: {
    light: ["light", "blue", "green", "purple", "ocean"],
    dark: ["dark"],
  },
  themeStorageKey: "omni-theme-vanilla-demo",
  enableSystem: true,
  enableColorScheme: true,
  updateClassAttribute: true,
  dataAttributes: ["data-theme"],
  forcedThemeFlagAttribute: "data-theme-forced",
  debug: false,
});

// Export types for TypeScript usage
export type { ThemeName } from "@madooei/omni-themes";

Let’s break down the options used in this example:

React Integration

import React, { useEffect, useState } from "react";
import { $theme, setTheme, themes } from "./theme-store";

function useTheme() {
  const [theme, setThemeState] = useState($theme.get());

  useEffect(() => {
    const unsubscribe = $theme.listen(setThemeState);
    return unsubscribe;
  }, []);

  return { theme, setTheme, themes };
}

function ThemeSelector() {
  const { theme, setTheme, themes } = useTheme();

  return (
    <select value={theme} onChange={(e) => setTheme(e.target.value)}>
      {themes.map((themeName) => (
        <option key={themeName} value={themeName}>
          {themeName.charAt(0).toUpperCase() + themeName.slice(1)}
        </option>
      ))}
    </select>
  );
}

Configuration Options

createThemeStore

The main function to create a theme store with the following options:

Configuration:

Returns:

Styling with CSS Custom Properties

Omni-Themes works with CSS Custom Properties (CSS variables) for flexible theming. You define themes using CSS variables and Omni-Themes manages applying them.

Setup CSS Themes:

  1. Define your themes using CSS custom properties:

    /* Default/Light theme */
    :root {
      --bg-primary: #ffffff;
      --text-primary: #1e293b;
      --accent-primary: #3b82f6;
    }
    
    /* Dark theme */
    [data-theme="dark"] {
      --bg-primary: #0f172a;
      --text-primary: #f1f5f9;
      --accent-primary: #60a5fa;
    }
    
    /* Custom theme */
    [data-theme="ocean"] {
      --bg-primary: #ecfeff;
      --text-primary: #155e75;
      --accent-primary: #06b6d4;
    }
    
  2. Use the CSS variables in your styles:

    body {
      background-color: var(--bg-primary);
      color: var(--text-primary);
      transition:
        background-color 0.3s ease,
        color 0.3s ease;
    }
    
    .button {
      background-color: var(--accent-primary);
      border-radius: 0.375rem;
    }
    

Advanced Features

FOUC (Flash of Unstyled Content) Prevention

Omni-Themes prevents theme flashing during page load by providing inline scripts:

<!DOCTYPE html>
<html>
  <head>
    <!-- Inject theme script before any content -->
    <script id="theme-script"></script>
  </head>
  <body>
    <!-- Your app content -->
  </body>
</html>
// Inject the FOUC prevention script
const themeScript = document.getElementById("theme-script");
if (themeScript) {
  themeScript.innerHTML = applyThemeScriptString;
}

Cross-Tab Synchronization

Theme changes automatically sync across browser tabs using localStorage events. No additional setup required.

System Theme Detection

When enableSystem: true, the library automatically detects system dark/light mode preferences and updates accordingly.

Cloning the Repository

To make your workflow more organized, it’s a good idea to clone this repository into a directory named omni-themes-workspace. This helps differentiate the workspace from the omni-themes located in the packages directory.

git clone https://github.com/madooei/omni-themes omni-themes-workspace

cd omni-themes-workspace

Repository Structure

How to Use This Repo

Using a VSCode Multi-root Workspace

With Visual Studio Code, you can enhance your development experience by using a multi-root workspace to access packages, examples, and playgrounds simultaneously. This approach is more efficient than opening the root directory, or each package or example separately.

To set up a multi-root workspace:

  1. Open Visual Studio Code.
  2. Navigate to File > Open Workspace from File....
  3. Select the omni-themes.code-workspace file located at the root of the repository. This action will open all specified folders in one workspace.

The omni-themes.code-workspace file can be customized to include different folders or settings. Here’s a typical configuration:

{
  "folders": [
    {
      "path": "packages/omni-themes"
    },
    {
      "path": "examples/react"
    },
    {
      "path": "examples/astro"
    }
  ],
  "settings": {
    // Add any workspace-specific settings here, for example:
    "git.openRepositoryInParentFolders": "always"
  }
}

Developing the Package

Change to the package directory and install dependencies:

cd packages/omni-themes
npm install

Package Management

When you are ready to publish your package:

npm run release

This single command will:

[!TIP] For detailed information about package publishing, versioning, and local development workflows, see the NPM Package Management Guide.