dark-reader-aware-theme-toggle-button
Version:
Dark mode toggle button that detects the Dark Reader extension and steps aside
206 lines (143 loc) • 6.98 kB
Markdown
# dark-reader-aware-theme-toggle-button
Dark mode toggle button that **steps aside when [Dark Reader](https://darkreader.org/) is active** — so your site's theme and the extension don't fight.
By [Tim Bailey-Jones](https://linuxtampa.com) (`timbaileyjones` on npm).
Source: [github.com/timbaileyjones/dark-reader-aware-theme-toggle-button](https://github.com/timbaileyjones/dark-reader-aware-theme-toggle-button)
## Why
Sites with their own dark mode toggle can clash with Dark Reader: double-inversion, muddy colors, broken contrast. The polite fix is to detect Dark Reader and hide your toggle while it's doing the job.
Dark Reader uses different DOM signals depending on mode:
| DR mode | Signals |
|---------|---------|
| Dynamic | `data-darkreader-scheme`, `data-darkreader-mode`, `meta[name="darkreader"]` |
| Filter / Static | `data-darkreader-mode`, `#dark-reader-style` (no `data-darkreader-scheme`) |
This library checks all of them and watches for late injection via `MutationObserver`.
I wrote up the background and design decisions behind this in more detail: [Building a Dark Mode Toggle That Plays Nice With Dark Reader](https://linuxtampa.com/blog/2026-03-18-dark-mode-and-dark-reader/).
## Install
```bash
npm install dark-reader-aware-theme-toggle-button
```
## Quick start
### With a bundler (recommended)
Use the package name — your bundler resolves `node_modules` for you. Do **not** link to `/node_modules/...` in HTML; that path is not served in production.
```javascript
import { mountThemeToggleButton } from 'dark-reader-aware-theme-toggle-button';
import 'dark-reader-aware-theme-toggle-button/styles/toggle-button.css';
mountThemeToggleButton({
button: '#theme-toggle',
label: '#theme-toggle-text',
});
```
Works with Vite, webpack, Rollup, esbuild, Parcel, and similar tools.
### Static HTML via CDN
For plain HTML with no build step, load published files from a CDN. **Pin the version** in production (replace `0.1.3` with the version you want):
```html
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/dark-reader-aware-theme-toggle-button@0.1.3/styles/toggle-button.css"
>
<!-- Your site CSS: define :root and [data-theme="dark"] variables -->
<button class="dra-theme-toggle" id="theme-toggle" aria-label="Toggle dark mode">
<span id="theme-toggle-text">Dark</span>
</button>
<script src="https://cdn.jsdelivr.net/npm/dark-reader-aware-theme-toggle-button@0.1.3/dist/global.js"></script>
<script>
DarkReaderAwareThemeToggle.mount({
button: '#theme-toggle',
label: '#theme-toggle-text',
});
</script>
```
[jsDelivr](https://www.jsdelivr.com/package/npm/dark-reader-aware-theme-toggle-button) and [unpkg](https://unpkg.com/dark-reader-aware-theme-toggle-button/) both mirror npm. Equivalent unpkg URLs:
```html
<link rel="stylesheet" href="https://unpkg.com/dark-reader-aware-theme-toggle-button@0.1.3/styles/toggle-button.css">
<script src="https://unpkg.com/dark-reader-aware-theme-toggle-button@0.1.3/dist/global.js"></script>
```
### Static site with `npm install` (copy at build time)
If you run `npm install` but ship static files (Eleventy, Hugo, Jekyll, etc.), copy assets from `node_modules` into your published output during the build. Browsers never read `node_modules` directly.
**One-off copy** (adjust destination paths to match your site):
```bash
cp node_modules/dark-reader-aware-theme-toggle-button/dist/global.js src/js/dra-theme-toggle.js
cp node_modules/dark-reader-aware-theme-toggle-button/styles/toggle-button.css src/css/dra-theme-toggle.css
```
Then reference your copied paths:
```html
<link rel="stylesheet" href="/css/dra-theme-toggle.css">
<script src="/js/dra-theme-toggle.js"></script>
```
**Eleventy passthrough** (example):
```javascript
// .eleventy.js
export default function (eleventyConfig) {
eleventyConfig.addPassthroughCopy({
'node_modules/dark-reader-aware-theme-toggle-button/dist/global.js': 'js/dra-theme-toggle.js',
'node_modules/dark-reader-aware-theme-toggle-button/styles/toggle-button.css': 'css/dra-theme-toggle.css',
});
}
```
Add a `postinstall` or pre-build script if you want copies to stay in sync automatically after `npm install`.
## Theming contract
This package toggles `data-theme="dark"` on `<html>` and persists to `localStorage`. **Your CSS** defines the palette:
```css
:root {
--bg-body: #ede4d8;
--text-primary: #2c1a0e;
}
[data-theme="dark"] {
--bg-body: #0a0f0a;
--text-primary: #00ff9f;
}
body {
background: var(--bg-body);
color: var(--text-primary);
}
```
## API
### `mountThemeToggleButton(options)`
Wire a button to toggle theme, update labels, hide when DR is active.
| Option | Default | Description |
|--------|---------|-------------|
| `button` | — | Element or CSS selector (required) |
| `label` | — | Label element or selector |
| `labels.light` | `'Dark'` | Button text in light mode |
| `labels.dark` | `'Light'` | Button text in dark mode |
| `storageKey` | `'theme'` | localStorage key |
| `attribute` | `'data-theme'` | HTML attribute on `<html>` |
| `darkValue` | `'dark'` | Attribute value for dark mode |
| `respectSystemPreference` | `false` | Use `prefers-color-scheme` on first visit |
| `hideWhenDarkReaderActive` | `true` | Hide button while DR runs |
| `onThemeChange` | — | `(theme) => void` callback |
Returns an `unmount()` function.
The IIFE / CDN global exposes the same function as `DarkReaderAwareThemeToggle.mount()`.
### Lower-level exports
- `isDarkReaderActive()` — synchronous check
- `watchDarkReader(onChange)` — observer; returns unsubscribe
- `getTheme()`, `setTheme()`, `toggleTheme()`, `initThemeEarly()`
### Avoid flash of wrong theme
Apply the saved theme before the first paint.
**With a bundler** — inline in `<head>` before CSS:
```html
<script type="module">
import { initThemeEarly } from 'dark-reader-aware-theme-toggle-button';
initThemeEarly();
</script>
```
**Static HTML (CDN or copied IIFE)** — blocking script in `<head>` before CSS:
```html
<script src="https://cdn.jsdelivr.net/npm/dark-reader-aware-theme-toggle-button@0.1.3/dist/global.js"></script>
<script>DarkReaderAwareThemeToggle.initThemeEarly();</script>
```
Load the bundle once; reuse it for `mount()` at the end of `<body>` if you prefer.
## Package exports
| Import path | File |
|-------------|------|
| `dark-reader-aware-theme-toggle-button` | ESM / CJS API (`dist/index.js`, `dist/index.cjs`) |
| `dark-reader-aware-theme-toggle-button/styles/toggle-button.css` | Default button styles |
| `dark-reader-aware-theme-toggle-button/global` | IIFE entry (usually loaded via `<script src=".../dist/global.js">` instead) |
## Design notes
- Uses `visibility: hidden` (not `display: none`) when hiding from Dark Reader so nav layout doesn't shift.
- Zero runtime dependencies.
- TypeScript types included.
## Used on
- [linuxtampa.com](https://linuxtampa.com)
- [bailey-jones.com](https://bailey-jones.com)
## License
MIT