Count Working Days in JavaScript With UK Bank Holidays
Count and add working days in JavaScript with UTC dates, skip weekends and UK bank holidays from gov.uk's JSON, and avoid the clocks-change bug.
To build a dark mode toggle with JavaScript and localStorage, define your colours as CSS variables, override them under :root[data-theme="dark"], and have a button switch the data-theme attribute on the <html> element and save the choice with localStorage.setItem("theme", ...). The key detail is a tiny script in the <head> that reads the saved theme, or falls back to the system setting with prefers-color-scheme, before the page is painted, so there's no white flash on reload. The complete, tested code is below.
My portfolio has a dark theme by default, and the first version of my light/dark switch had the classic bug: every page load flashed white for a split second before the dark colours kicked in. This version fixes that. I tested it in headless Chrome twice, once with the system set to light and once set to dark, clicking the toggle and reloading the page each time.
--bg and --text.<html data-theme="dark"> is set.<head> picks the theme: the saved choice if there is one, otherwise the operating system's setting.localStorage, so it's remembered next time.<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<title>Dark mode toggle</title>
<script>
// Runs before the page is painted, so there is no white flash
(function () {
let theme = null;
try { theme = localStorage.getItem("theme"); } catch (e) {}
if (theme !== "light" && theme !== "dark") {
theme = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
document.documentElement.dataset.theme = theme;
})();
</script>
<link rel="stylesheet" href="theme.css">
</head>
<body>
<header>
<button id="theme-toggle" type="button" aria-pressed="false">Dark mode</button>
</header>
<main>
<h1>My portfolio</h1>
<p>Click the button to switch themes. Your choice is remembered.</p>
</main>
<script src="theme.js"></script>
</body>
</html>
The inline script is deliberately not in an external file or at the bottom of the page. Scripts in the <head> run before the browser paints the body, so the right data-theme is already set when the stylesheet is applied. If you moved this code into theme.js at the end of the body, the page would draw in the default theme first and then switch, which is the flash.
Two more details are worth pointing out:
try/catch around localStorage: some browsers block storage entirely in strict privacy modes, and reading it then throws an error. Without the try, the whole script would stop and no theme would be set."light" or "dark": anything else in storage (an old value, or something typed into DevTools) is ignored, and the system setting is used instead.The <meta name="color-scheme"> tag tells the browser the page supports both, so things like scrollbars and form controls can match. The <header> and <main> elements are there for structure; I explain why in HTML semantic tags explained with examples.
:root {
--bg: #ffffff;
--text: #1a1d24;
--accent: #4b3fd6;
}
:root[data-theme="dark"] {
--bg: #10131c;
--text: #e4e8f2;
--accent: #8b7cff;
}
body {
margin: 0;
padding: 2rem;
background: var(--bg);
color: var(--text);
font-family: system-ui, sans-serif;
transition: background-color 0.2s, color 0.2s;
}
#theme-toggle {
border: 2px solid var(--accent);
background: transparent;
color: var(--text);
padding: 0.5rem 1rem;
border-radius: 999px;
cursor: pointer;
}
Because every colour on the page comes from a variable, switching themes is just a matter of changing four or five values in one place. No class needs to be added to every element. As your site grows, add variables for borders, cards and links, and never hard-code a colour in a component. The transition makes the switch fade gently instead of snapping.
// theme.js: the toggle button
const root = document.documentElement;
const button = document.getElementById("theme-toggle");
function applyTheme(theme) {
root.dataset.theme = theme;
button.setAttribute("aria-pressed", String(theme === "dark"));
}
function saveTheme(theme) {
try {
localStorage.setItem("theme", theme);
} catch (e) {
// Storage blocked: the toggle still works, it just won't be remembered
}
}
applyTheme(root.dataset.theme);
button.addEventListener("click", () => {
const next = root.dataset.theme === "dark" ? "light" : "dark";
applyTheme(next);
saveTheme(next);
});
The button uses aria-pressed so that screen readers announce it as a toggle that's either on or off. The label stays "Dark mode" and the pressed state says whether it's active. That's clearer than changing the text to "Light mode", where a screen reader user can't tell if the label describes the current state or the action.
Saving also has a try/catch. If storage is full or blocked, the theme still changes for this page; it just isn't remembered. I used the same defensive approach when saving data in my habit tracker with JavaScript and localStorage.
I loaded the page in headless Chrome inside a small test harness, recorded the state, clicked the button, recorded it again, reloaded, and recorded it a third time. I ran it once with Chrome's colour scheme set to light and once set to dark:
# System set to light
first visit: data-theme=light, aria-pressed=false, background=rgb(255, 255, 255), saved=null
after click: data-theme=dark, aria-pressed=true, background=rgb(16, 19, 28), saved=dark
after reload: data-theme=dark, aria-pressed=true, background=rgb(16, 19, 28), saved=dark
# System set to dark
first visit: data-theme=dark, aria-pressed=true, background=rgb(16, 19, 28), saved=null
after click: data-theme=light, aria-pressed=false, background=rgb(255, 255, 255), saved=light
after reload: data-theme=light, aria-pressed=false, background=rgb(255, 255, 255), saved=light
Everything behaved as expected:
saved=null), so the page followed the system setting.The code above follows the operating system only on a first visit. If you'd like the page to change live when someone's laptop switches to dark mode at sunset, listen for changes to the media query and only react if the user hasn't picked a theme yet:
matchMedia("(prefers-color-scheme: dark)").addEventListener("change", (e) => {
if (!localStorage.getItem("theme")) applyTheme(e.matches ? "dark" : "light");
});
I couldn't flip the operating system setting halfway through a headless test, so I checked this listener's logic in Node with a fake matchMedia and localStorage:
no saved choice, system goes dark -> dark
saved 'light', system goes dark -> light
You can read more about the media query on MDN's prefers-color-scheme page. To give users a way back to "use my system setting", add a third option that calls localStorage.removeItem("theme").
DOMContentLoaded. Move the reading part into an inline script in the <head>.localStorage is shared per origin, so www.example.com and example.com have separate storage. Pick one and redirect the other.<picture> element with a media="(prefers-color-scheme: dark)" source, or give dark logos a light background.suppressHydrationWarning to the <html> element.Partly. A hidden checkbox plus the :has() selector, such as :root:has(#dark:checked), can switch the variables without any JavaScript. But CSS can't remember anything, so the choice is lost on reload. If remembering matters, and for a toggle it usually does, you need the few lines of JavaScript above.
When the user clicks the toggle, call localStorage.setItem("theme", "dark") (or "light"). On page load, read it back with localStorage.getItem("theme") and set the theme before the page is painted.
Put a small inline script in the <head>, before your stylesheet, that sets data-theme on the <html> element. Because it runs before the body is drawn, the first paint already uses the right colours.
Use window.matchMedia("(prefers-color-scheme: dark)").matches. It's true when the operating system or browser is set to dark.
localStorage is simpler and isn't sent with every request. Use a cookie only if your server needs to render the right theme in the HTML itself, for example in some server-rendered frameworks.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Count and add working days in JavaScript with UTC dates, skip weekends and UK bank holidays from gov.uk's JSON, and avoid the clocks-change bug.
Work out a First, 2:1 or 2:2 from module marks: credit-weighted averages, 30:70 or 1:2 year weightings, rounding at 69.5 and the mark you need.
How UK student loan repayments are worked out in 2026/27: per-period thresholds, 9% over, rounding down, two plans at once and a floating-point trap.