# theme-change: CSS theme controls backed by localStorage

> theme-change is a small JavaScript helper that connects CSS custom properties to buttons, selects, checkboxes, and persisted theme choices. It fits small sites and component-based applications that want a declarative theming layer without a larger state manager.

**saadeghi/theme-change** — Change CSS theme with toggle, buttons or select using CSS custom properties and localStorage

- Repository: https://github.com/saadeghi/theme-change
- Website: https://codepen.io/saadeghi/pen/OJypbNM
- Stars: 1,714 · Forks: 48
- Language: JavaScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/saadeghi-theme-change

## The control layer theme-change adds to CSS variables

theme-change solves a narrow but common front-end problem: connecting user-selected themes to CSS without writing bespoke event handlers for every button, select, and checkbox. CSS custom properties define the visual values, while the JavaScript helper changes the relevant HTML attribute so the browser selects a different token set. The project is aimed at developers who want compact theme controls without adding a runtime dependency. Its package metadata lists no dependencies, and the documented API centers on one function, `themeChange()`.

The library does not create the color palette or determine what a dark theme should mean. Those decisions stay in CSS. A small theme definition might look like this:

```css
:root {
  --my-color: #fff;
}
[data-theme="dark"] {
  --my-color: #000;
}
[data-theme="pink"] {
  --my-color: #ffabc8;
}
body {
  background-color: var(--my-color);
}
```

This separation makes the helper suitable for custom token systems and for daisyUI themes, which the README explicitly mentions. It also sets a boundary: theme-change handles selection, synchronization, and browser-local persistence, but it is not a token generator, theme designer, or server-backed preferences service.

The package version in `package.json` is `3.0.4`. There are no GitHub releases, so the repository history does not provide a published release sequence through GitHub. Upgrade planning therefore cannot be based on release notes available from that channel.

## One data-set-theme attribute controls common inputs

The core interface is the `data-set-theme` HTML attribute. Its meaning depends partly on the control type. A button can select one fixed theme, such as dark or pink. A button can also list several themes separated by commas, in which case each activation advances to the next item and wraps around. A select obtains the selected option value, while a checkbox sets its configured `value` when checked and clears the theme when unchecked.

A direct dark-theme button requires very little markup:

```html
<button data-set-theme="dark">Dark</button>
```

For a rotating control, the order is explicit:

```html
<button data-set-theme="dark,light,pink">Rotate</button>
```

A select uses option values as theme names:

```html
<select data-set-theme>
  <option value="">Default</option>
  <option value="dark">Dark</option>
</select>
```

The checkbox pattern follows the binary semantics of the control:

```html
<input type="checkbox" value="dark" data-set-theme />
```

These patterns are useful because component templates can participate in theme selection without each component implementing its own storage or synchronization logic. They do not enforce accessible labels, labels for the select, or sensible initial UI state. For example, the README's button examples contain visible text, while the checkbox example does not show a label. The project provides a way to set `aria-pressed` or `aria-current` through `data-act-attribute`, but application authors remain responsible for applying appropriate accessible markup.

## CDN and npm installation for a first page

The quickest documented route is the jsDelivr CDN script. Add it to an HTML page, define theme values in CSS, and include a control with `data-set-theme`. The CDN example uses the floating `latest` tag, so the exact version resolved by the browser is not fixed in the documentation:

```html
<script src="https://cdn.jsdelivr.net/npm/theme-change@latest/index.js"></script>
```

The page should then load CSS that defines the values used by the selected theme. The script handles the control convention, while the page author supplies the visual design.

For an npm-based JavaScript application, install the package with the documented command:

```bash
npm i theme-change@latest
```

Then initialize the helper during normal page setup:

```js
import { themeChange } from "theme-change";

themeChange();
```

The package is an ES module. Its `package.json` declares `"type": "module"` and exposes the main entry plus `./btn`, `./select`, and `./toggle` subpaths with matching TypeScript declaration files. The README does not document runtime configuration options, custom storage keys, cross-tab synchronization, or a server rendering API.

## Mounted controls need the false initialization mode

Initialization timing is the main implementation detail for framework users. The README uses `themeChange(false)` when controls are mounted after the initial page load. That call is documented for React, Vue, Svelte, and Astro, which indicates that component render timing is considered part of normal integration rather than an exceptional case.

In React, initialization occurs inside `useEffect`:

```js
import { useEffect } from "react";
import { themeChange } from "theme-change";

useEffect(() => {
  themeChange(false);
}, []);
```

Vue follows the same principle through `onMounted`:

```js
import { onMounted } from "vue";
import { themeChange } from "theme-change";

export default {
  setup() {
    onMounted(() => {
      themeChange(false);
    });
  },
};
```

Svelte calls the false form from `onMount`:

```js
import { onMount } from "svelte";
import { themeChange } from "theme-change";

onMount(() => {
  themeChange(false);
});
```

The Astro example places the call in a client script:

```astro
---
import { themeChange } from "theme-change";
---

<script>
  themeChange(false);
</script>
```

The precise event mechanism behind the boolean argument is not described in the README, so it would be unsafe to claim whether it represents delayed scanning, event delegation, or another implementation detail. What is confirmed is the supported call form and its intended use for later-mounted controls.

## localStorage persistence is browser-local and not account-backed

Theme choices are persisted in `localStorage`, which makes a selection survive navigation and reloads within the same browser context. This is useful for public sites and demos where no authenticated account exists. It also creates an important boundary: the documented mechanism is browser-local storage, not a remote user profile. The README does not claim synchronization between users, browsers, or devices.

The helper also synchronizes controls that share the same `data-key`. That is more specific than merely making every matching control react. It allows separate control groups as shown by the documented names:

```html
<select data-set-theme data-key="admin-panel"></select>
<button data-set-theme="dark" data-key="front-page"></button>
<input type="checkbox" value="pink" data-set-theme data-key="premium-user-theme" />
```

The README does not fully define whether the default controls are globally synchronized or how `data-key` affects persistence namespace details. It only states that controls with the same `data-key` stay in sync. That is enough to establish a grouping mechanism, but not enough to infer server synchronization, user isolation, cookie fallback, or quota handling.

This makes localStorage unsuitable as the sole solution when theme preference must follow an authenticated user across devices. A server application would need a separate persistence layer and a defined relationship between server preferences and any browser-local state. The README documents no API for that integration.

## Active-state attributes work on buttons, not every control

The optional `data-act-class` attribute applies a class to a button while its theme is active and removes it when the theme is inactive. `data-act-attribute` offers a more flexible equivalent by setting an arbitrary HTML attribute. Its syntax is `name[:value]`, and only the first colon separates the name from the value. That detail allows values that contain additional colons.

For example, the following active state uses an explicit value:

```html
<button data-set-theme="dark" data-act-attribute="aria-pressed:true"></button>
```

A bare attribute name represents boolean presence:

```html
<button data-set-theme="pink" data-act-attribute="aria-current"></button>
```

According to the README, the first example has `aria-pressed="true"` while active and removes the attribute while inactive. The second has `aria-current=""` while active and is removed otherwise. `data-act-class` and `data-act-attribute` can be combined on the same element.

There is a real limitation in the scope: both active-state features are button-only and are ignored on select and checkbox controls. An application whose main interface is a select therefore cannot assume that the helper will mark the currently active option or wrapper through these attributes. It must handle that state elsewhere. The button-only restriction makes sense for simple toggle-button groups, but it should influence control design.

## Backward compatibility, package shape, and maintenance boundaries

Version 3 retains two legacy attribute names from version 2: `data-toggle-theme` and `data-choose-theme`. The README says they still work. That compatibility is useful when maintaining an older template, but it can also leave two naming conventions in one codebase. A new implementation should still use `data-set-theme` unless maintaining existing markup has a specific reason.

The repository contains source files, TypeScript declaration files, an `index.html`, `build.mjs`, `package.json`, and build configuration. The only package script shown is:

```bash
npm run build
```

The package publishes compiled JavaScript variants for the main, button, select, and toggle entry points. Its declared runtime dependencies list is empty, reducing package installation concerns to the package itself, although that does not by itself establish application bundle size. The README includes badges for bundle and file size, but no numeric size is documented in the available text.

The repository is not archived, and its last push was on 2026-06-14. That is the most recent maintenance fact available. There are no GitHub releases, so upgrade notes cannot be checked through the releases page. Because the project uses `@latest` in both documented installation methods, deployments can receive a newer package without an application commit. Production users who need repeatable builds should inspect the package version in their lockfile before changing it.

## Why a full theme manager is usually unnecessary for theme-change

A real alternative is to keep theme selection in the application's existing client-side state layer and update the HTML attribute through that layer. This approach can work well when the application already has a global store, a server-rendered user preference, an accessibility policy, or a design-system theme provider. The application already knows when controls mount, which preference belongs to a user, and how state should be restored. Adding theme-change on top may duplicate state handling rather than remove it.

theme-change offers a different tradeoff. Its declarative controls, localStorage persistence, and matching-control synchronization are available through HTML attributes with a small package and no declared runtime dependencies. It is the better fit for a static page, a CodePen experiment, or a lightweight component library where the page author already controls the CSS token structure and does not need account-level persistence.

The package is the wrong choice for a product that requires theme preferences stored on a server, synchronized across devices, or governed by authenticated roles. It is also a poor replacement for a design token pipeline because the README treats token values as application-defined CSS. A direct state-based implementation may require more application code, but it provides clearer ownership of persistence and rendering. The relevant comparison is not feature count. It is whether the project needs a local declarative control helper or an application-wide preference system.

## Conclusion

Adopt theme-change when the application already expresses themes through CSS variables or a compatible CSS framework and needs declarative controls with localStorage persistence. Do not choose it as a complete design-system theme manager, a server-side user preference system, or a replacement for application state. Verify first that the required token model, persistence semantics, and mounted-control timing fit the application, especially when controls render after the initial page load.

## FAQ

### How do I install and initialize theme-change?

Install it with `npm i theme-change@latest`, import `themeChange` from `theme-change`, and call `themeChange()`. The README also documents a jsDelivr CDN script for direct HTML use.

### Does theme-change support React, Vue, Svelte, and Astro?

Yes. The README provides framework snippets that call `themeChange(false)` in the framework's mount lifecycle. This form is intended for controls mounted after initial page load.

### Does theme-change persist themes and synchronize controls?

Yes. The helper stores the selected theme in `localStorage`, and controls with the same `data-key` stay in sync. The documentation does not describe server-side or cross-device preference storage.

## Sources

- [Issues](https://github.com/saadeghi/theme-change/issues)
- [License: MIT](https://github.com/saadeghi/theme-change/blob/master/LICENSE)
- [Project website](https://codepen.io/saadeghi/pen/OJypbNM)
- [README](https://github.com/saadeghi/theme-change/blob/master/README.md)
- [saadeghi/theme-change on GitHub](https://github.com/saadeghi/theme-change)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/saadeghi-theme-change
