<solar-eclipse-toggle> Web Component
An accessible light/dark theme toggle button that follows your system preference by default.
Installation
npm install @zachleat/solar-eclipse-toggle
Include the stylesheet and the script on your web site:
<link rel="stylesheet" href="solar-eclipse-toggle.css">
<script type="module" src="solar-eclipse-toggle.js"></script>
The stylesheet is also exported as @zachleat/solar-eclipse-toggle/style.css.
Usage
Define the icons once as reusable symbols (copy the full paths from demo.html):
<svg aria-hidden="true" style="position: absolute; width: 0; height: 0; overflow: hidden">
<symbol id="se-icon-moon" viewBox="0 0 512 512"></symbol>
<symbol id="se-icon-sun" viewBox="0 -32 576 576"></symbol>
<symbol id="se-icon-half" viewBox="0 0 512 512"></symbol>
</svg>
Then use the component markup:
<solar-eclipse-toggle>
<button type="button" disabled>
<span class="se-label-dark" hidden><svg aria-hidden="true"><use href="#se-icon-moon"/></svg>Use dark theme</span>
<span class="se-label-light" hidden><svg aria-hidden="true"><use href="#se-icon-sun"/></svg>Use light theme</span>
<span class="se-label-unknown"><svg aria-hidden="true"><use href="#se-icon-half"/></svg>Theme</span>
<span class="se-system">System</span>
</button>
</solar-eclipse-toggle>
Add this to your <head> to apply a saved theme before first paint:
<script>
try {
var theme = localStorage.getItem("theme");
if(theme === "light" || theme === "dark") {
document.documentElement.setAttribute("data-theme", theme);
}
} catch(e) {}
</script>
Write your dark styles for both the system preference and the override:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
}
}
:root[data-theme="dark"] {
color-scheme: dark;
}
Default theme
To ignore the system preference when there is no saved choice, add data-theme-default (light or dark) to the root element:
<html data-theme-default="dark">
Use the same attribute in your own CSS to pick the page’s default colors:
:root {
color-scheme: dark;
}
:root[data-theme="light"] {
color-scheme: light;
}
Consider relabeling SYSTEM (e.g. <span class="se-system">Default</span>) and setting status-auto, which defaults to the default when a default theme is declared. See demo-default-dark.html.
Features
- Follows
prefers-color-scheme until a visitor picks the other theme, which is saved to localStorage.
- Choosing the system theme again clears the saved override.
- A plain
<button> named for what it does next (“Use dark theme”), with no aria-pressed state.
- Announces the new theme in a
role="status" live region, since screen readers don’t reliably announce name changes.
- Labels, icons, and the SYSTEM label switch with CSS, so the button width never changes.
- Before JavaScript runs (or without it), the button is disabled and shows
.se-label-unknown with SYSTEM.
- Without the stylesheet,
hidden on .se-label-dark and .se-label-light keeps only .se-label-unknown visible.
- Multiple instances, other tabs, and live system preference changes all stay in sync.
Options
storage-key: localStorage key. Default: theme
status-light, status-dark, status-auto: announcements. Defaults: Light theme on, Dark theme on, matching your system (or the default with data-theme-default)
- Listen for the bubbling
theme-change event, with event.detail of { theme, auto }.
- Translate by editing the markup and the
status-* attributes.
Skip automatic definition
Add ?nodefine to the script URL to skip the customElements.define call:
<script type="module">
import { SolarEclipseToggle } from "./solar-eclipse-toggle.js?nodefine";
SolarEclipseToggle.define();
</script>
The stylesheet targets the solar-eclipse-toggle tag name.
Styling
Styles are in the solar-eclipse-toggle cascade layer, so your own styles win. Colors use custom properties, with light and dark naming the current page theme:
--se-light-color, --se-light-border, --se-light-icon, --se-light-hover, --se-light-system-bg, --se-light-system-color
--se-dark-color, --se-dark-border, --se-dark-icon, --se-dark-hover, --se-dark-system-bg, --se-dark-system-color
--se-focus
Credits
Demo icons are Font Awesome Free (CC BY 4.0).