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:
- Written in TypeScript
- Builds to both modern ES modules and CommonJS formats
- Provides TypeScript type definitions
- ESLint for code linting
- Prettier for code formatting
- Vitest for testing
- Tsup for building
- Minimal dependencies
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:
get(): T
- Return the current value of the atomlisten(callback: (value: T, oldValue: T) => void)
- Listen for changes to the atomsubscribe(callback: (value: T) => void)
- Subscribe to the atom
[!NOTE]
subscribe
calls the callback immediately with the store’s current value and then on every future change, whilelisten
only calls the callback when the value changes (not at subscription time). Usesubscribe
when you want the current state right away; uselisten
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:
-
themes
: An array of available theme names. You can use this to populate a theme selector UI. In the default configuration, it includes['light', 'dark', 'system]
, but you can customize it. (I’ll explain how to customize themes later.) -
setTheme
: A function to change the current theme. It accepts any theme name from thethemes
array. For example,setTheme('dark')
will change the theme to dark mode. -
$theme
and$resolvedTheme
: These are Nanostores atoms that hold the current theme and resolved theme respectively. What is the difference? Well, let’s say you dosetTheme('system')
. The$theme
atom will be set to'system'
, but the$resolvedTheme
atom will be set to either'light'
or'dark'
based on the user’s system preference. This way, you can always get the actual theme being applied to your app. You can further customize this behavior by providing options to thecreateThemeStore
function (will explain later). -
$systemTheme
: This atom holds the user’s system preference for light or dark mode. It will beundefined
if the system preference is not set or not detectable. -
applyThemeScriptString
: A string containing a script that you can inject into your HTML to prevent Flash of Unstyled Content (FOUC) when the theme changes. This script should be included in the<head>
section of your HTML. You can do this in a few different ways, depending on your framework or setup. For example, in React, you can useuseEffect
to inject it into the document head. In vanilla JavaScript, you can do this:document.head.insertAdjacentHTML( "beforeend", `<script>${applyThemeScriptString}</script>`, );
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:
-
themes
: An array of theme names. You can define any number of themes here. If you don’t specify this option, it defaults to['light', 'dark']
. Do not include thesystem
theme here, as it is handled automatically by the library. If you want to use the system theme, setenableSystem: true
(which is the default). -
defaultTheme
: The default theme to use when the app loads. It can be any theme from thethemes
array. If not specified, it defaults to the first theme in thethemes
array unlessenableSystem
is set totrue
, in which case it defaults to the system preference. -
defaultLightTheme
anddefaultDarkTheme
: These specify which themes to resolve to when the user selects thesystem
theme. So, if we have multiple themes and the user selectssystem
, and the system is in light mode, the app will use thedefaultLightTheme
. If the system is in dark mode, it will use thedefaultDarkTheme
. If not specified, it defaults tolight
for light mode anddark
for dark mode, assuming those themes are defined in thethemes
array. If not, it. defaults to the first theme in thethemes
array for light mode and the last theme for dark mode. -
themesMap
: This is an optional mapping of themes to light and dark modes. This works in conjunction with theenableColorScheme
option (will be explained shortly). -
themeStorageKey
: The key used to store the current theme inlocalStorage
. This allows the theme to persist across page reloads. You can change this to any string you like. By default, it is set toomni-theme
. -
enableSystem
: If set totrue
, the library will automatically detect the user’s system preference for light or dark mode and apply it. The default istrue
. -
enableColorScheme
: If set totrue
, the library will update the browser’s color-scheme meta tag based on the current theme. This is useful for browsers that support color schemes. The default istrue
. When you have multiple themes, you can use thethemesMap
option to specify which themes should be mapped to light and dark color schemes. IfenableColorScheme
istrue
butthemesMap
is not provided, it will set the color scheme to null, which means it won’t update the color scheme meta tag. -
updateClassAttribute
: If set totrue
, the library will update theclass
attribute of the<html>
element with the current theme. This is useful for CSS styling based on the theme. The default istrue
. -
dataAttributes
: An array of HTML data attributes to set on the<html>
element. By default, it setsdata-theme
to the current theme. You can add more attributes if needed. -
forcedThemeFlagAttribute
: The attribute used to indicate that a theme is forced. This is useful for pages that need to always use a specific theme regardless of user preference. The default isdata-theme-forced
. Notice that thecreateThemeStore
function also returns acreateForcedThemeScriptString
function that generates a script to force a specific theme on a page. This is useful for pages that need to always use a specific theme regardless of user preference. -
debug
: If set totrue
, the library will log debug information to the console. This is useful for development and debugging purposes. The default isfalse
.
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:
themes
(string[]) - Array of theme names (default: [‘light’, ‘dark’])enableSystem?
(boolean) - Enable system theme detection (default: true)enableColorScheme?
(boolean) - Update browser color-scheme (default: true)dataAttributes?
(string[]) - HTML data attributes to set (default: [‘data-theme’])classNames?
(string[]) - CSS class names to set (default: [])debug?
(boolean) - Enable debug logging (default: false)
Returns:
themes
(string[]) - Array of available themes$theme
(Atom) - Current theme atom (reactive)$resolvedTheme
(Atom) - Resolved theme atom (never ‘system’)-
$systemTheme
(Atom) - System preference atom (‘light’‘dark’ null) setTheme
(function) - Function to change themeapplyThemeScriptString
(string) - Script for FOUC preventioncreateForcedThemeScriptString
(function) - Generate forced theme script
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:
-
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; }
-
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
packages
— Contains the primary package(s) for this repository (e.g.,omni-themes
). Each package is self-contained and can be copied out and used independently.examples
— Contains examples of how to use the packages. Each example is a minimal, standalone project.playgrounds
— Contains demos of the dependencies of the primary package(s). Each playground is a minimal, standalone project.docs
— Contains various documentation for users and developers..github
— Contains GitHub-specific files, such as workflows and issue templates.
How to Use This Repo
- To work on a package, go to
packages/<package-name>
and follow its README. - To try an example, go to
examples/<example-name>
and follow its README. - To run the playground, go to
playground/<package-name>
and follow its README. - For documentation, see the
docs
folder.
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:
- Open Visual Studio Code.
- Navigate to
File > Open Workspace from File...
. - 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
- Read the Project Roadmap for project goals, status, evolution, and development guidelines.
- Read the Development Guide for detailed information on the package architecture, build configuration, and implementation patterns.
- Follow the Contributing Guide for contribution guidelines, coding standards, and best practices.
Package Management
When you are ready to publish your package:
npm run release
This single command will:
- Validate your code with the full validation pipeline
- Analyze commits to determine version bump
- Update package.json version and changelog
- Build the package
- Create and push git tag
- Create GitHub release
- Publish to NPM
[!TIP] For detailed information about package publishing, versioning, and local development workflows, see the NPM Package Management Guide.