Theme switcher
The theme lives in data-theme on
<html>. Theme buttons remember the choice in
localStorage and set aria-pressed for you.
The theme is data-theme on html
<html data-theme="dark"> or
"light" means a theme is chosen. Without the
attribute, the system theme (prefers-color-scheme) applies.
data-theme-toggle buttons change this value and save
it in localStorage under the key tsunami-theme. Every
colour variable has a second value for dark.
<html lang="en" data-theme="dark">
...
<div class="theme-switcher" role="group" aria-label="Theme">
<button type="button" data-theme-toggle="light" aria-label="Light"><i class="icon icon-sun" aria-hidden="true"></i></button>
<button type="button" data-theme-toggle="dark" aria-label="Dark"><i class="icon icon-moon" aria-hidden="true"></i></button>
<button type="button" data-theme-toggle="system" aria-label="System"><i class="icon icon-monitor" aria-hidden="true"></i></button>
</div>
Toggle
data-theme-toggle with no value switches between
light and dark, based on the current theme.
<button type="button" class="btn" data-theme-toggle>Toggle theme</button>
No flash on load
The script applies the saved theme as soon as it runs, so load it
as a plain script in <head>. With
type="module" or defer it runs after the
first paint, and people who chose dark see a white flash. If
localStorage is blocked, the theme still changes but is only kept
for that page.
<script src="https://cdn.jsdelivr.net/gh/Djirasak/tsunami-framework@v0.1.3/dist/tsunami.min.js"></script>
Icon and label follow the theme
data-theme-show="dark" shows only in dark and
"light" only in light. It is pure CSS (it reads
html[data-theme] or prefers-color-scheme), so it is
correct before JS loads.
<div class="stack" data-direction="row" data-gap="sm">
<button type="button" class="btn" data-variant="ghost" data-size="icon-sm" data-theme-toggle aria-label="Toggle theme">
<i class="icon icon-sun" data-theme-show="dark" aria-hidden="true"></i>
<i class="icon icon-moon" data-theme-show="light" aria-hidden="true"></i>
</button>
<button type="button" class="btn" data-size="sm" data-theme-toggle>
<i class="icon icon-sun" data-theme-show="dark" aria-hidden="true"></i>
<i class="icon icon-moon" data-theme-show="light" aria-hidden="true"></i>
<span data-theme-show="dark">Light mode</span>
<span data-theme-show="light">Dark mode</span>
</button>
</div>
set() picks a theme by name
window.tsunami.theme.set(name) takes
"light", "dark" or
"system". Use it to change the theme from code, for
example after a user saves their account settings.
<div class="stack" data-direction="row" data-gap="sm">
<button type="button" class="btn" data-size="sm" onclick="window.tsunami.theme.set('light')"><i class="icon icon-sun" aria-hidden="true"></i>Light</button>
<button type="button" class="btn" data-size="sm" onclick="window.tsunami.theme.set('dark')"><i class="icon icon-moon" aria-hidden="true"></i>Dark</button>
<button type="button" class="btn" data-size="sm" onclick="window.tsunami.theme.set('system')"><i class="icon icon-monitor" aria-hidden="true"></i>System</button>
</div>
Read with get(), listen for a change
get() returns "dark" or
"light", whichever is showing. The event fires on
document on every set() /
toggle() / button click, and when the system theme
changes while system is chosen.
<div class="stack" data-gap="sm">
<div>
<button type="button" class="btn" data-size="sm" onclick="window.tsunami.theme.toggle()">
Toggle
</button>
</div>
<p id="theme-result"></p>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const result = document.getElementById("theme-result");
result.textContent = "get() returns " + window.tsunami.theme.get();
document.addEventListener("tsunami:themechange", (e) => {
result.textContent =
"tsunami:themechange: theme = " + e.detail.theme + ", choice = " + e.detail.choice;
});
});
</script>
Components redraw on a change
CSS updates colours on its own. A canvas or iframe that the page
draws itself must listen for tsunami:themechange,
read the new colour variables and redraw.
<canvas id="theme-chart" style="width: 100%; max-width: 20rem; height: 7.5rem" aria-label="Sales, last 7 days" role="img"></canvas>
<script>
(() => {
const canvas = document.getElementById("theme-chart");
const sales = [12, 18, 9, 22, 16, 27, 20];
function draw() {
const css = getComputedStyle(canvas);
const w = canvas.clientWidth;
const h = canvas.clientHeight;
canvas.width = w * devicePixelRatio;
canvas.height = h * devicePixelRatio;
const ctx = canvas.getContext("2d");
ctx.scale(devicePixelRatio, devicePixelRatio);
ctx.fillStyle = css.getPropertyValue("--subtle");
ctx.fillRect(0, 0, w, h);
const bar = w / sales.length;
ctx.fillStyle = css.getPropertyValue("--accent");
sales.forEach((v, i) => {
const y = h - (v / 30) * h;
ctx.fillRect(i * bar + 4, y, bar - 8, h - y);
});
}
document.addEventListener("tsunami:themechange", draw);
draw();
})();
</script>
Attributes
| Attribute / element | Values | Description |
|---|---|---|
html[data-theme] |
(none = system, default), light,
dark
|
The chosen theme, set by JS and the head script. Without it, prefers-color-scheme applies. |
.theme-switcher |
div + role="group",
aria-label
|
Segmented frame for theme buttons. Children are
<button>s with icons.
|
data-theme-toggle |
(empty), light, dark,
system
|
Works on any button. Empty = toggle light / dark, any
other value = set(value);
system clears the saved choice.
|
aria-pressed |
true, false |
JS sets it on the data-theme-toggle button
whose value matches the current choice (system when there
is no data-theme).
|
aria-label |
text | Required on icon-only buttons. |
data-theme-show |
light, dark |
Shows the element only in that theme. Pure CSS. |
tsunami-theme |
light, dark |
localStorage key that holds the choice. Removed when system is chosen. |
API
Functions live on window.tsunami.theme.
| Function / event | Arguments / detail | Description |
|---|---|---|
theme.get() |
- |
Returns "dark" or "light" for
the theme on screen (including when it follows the
system).
|
theme.set(name) |
name: "light",
"dark", "system"
|
Sets and saves the theme. Any value other than light / dark counts as system. |
theme.toggle() |
- | Switches to the other theme from the one on screen. |
tsunami:themechange |
{ theme, choice } |
Fires on document after every
set() / toggle() / button click
(even if the value is unchanged), and when the system
theme changes while system is chosen. theme =
"dark" | "light",
choice = "light" |
"dark" | "system"
|