Lessons from the dark side

August 26th 2026

Over the last few weeks I’ve written up a little run of related notes: designing a set of icons that would sit happily in either theme, and then the design update for our programme hub (MA Web Design & Development) to add the new icons and a function to respect the visitor’s chosen view mode with a switcher to override it. This work was a pleasure, mostly because I planned for this during the icon redesign, ahead of the site update.

Then I turned to my handbook.

The handbook began as a place to gather my teaching materials, a box of links, tools, slide decks and references all in one findable spot. As I started collecting and re-organising my materials, dividing the content into chapters rather than plain sections felt right ~ and that single word pulled the whole site towards feeling like a book. That’s where the name came from, and the subtitle with it: a handbook for aspiring web designers. And a book, to me, meant a page, on paper ~ pale parchment with text in dark ink, light by design. So when it went live in 2023 with a single light theme, it was a deliberate choice in line with this very concept. Feedback was excellent, no one seemed to mind the light-only view mode.

What I didn’t plan for was how the ground would move. I’m someone who generally prefers light mode, especially for long reads. In time it became clear that many of my students and colleagues opt for dark mode when browsing the web. And they would expect it everywhere. And this is why I wanted to write about it: our job isn’t to build for our own taste, however considered. Our job is to build for what our users or readers need or prefer. A view-mode preference stopped being a stylistic flourish and became something we honour by default. No matter how much I loved my light-only book ~ I knew it had to adapt and offer a second theme ~ three years after the fact, which is really the harder way to do it; not something I’d recommend ツ

So this post is a summary of what I’ve learnt throughout this process, and also a warning to my future self: whatever the concept, plan for both view modes from the start ~ a design for the web with all its components needs to deliver both.

What follows is organised around the lessons rather than the projects. These are notes with generic, ready-to-copy examples you can drop into your own work ~ if you like; and references I can return to myself (◠‿◠)

Why this matters now

I elaborated on this in my Hub refresh, 2 modes post ~ so I’ll not repeat myself here; in short: a view-mode preference is no longer a nice-to-have we choose to implement. Rather, it’s a preference the visitor has already set ~ for whatever reason: tired eyes, light sensitivity, low vision, or if only a preference in the moment, in a dark room or due to the time of the day. Honouring this setting is akin to respecting reduced-motion or text-scaling. Offer both, and make neither an afterthought.

One aspect to add is the reading experience, because it is often a little misunderstood. Some tend to think of dark mode as something that makes on-screen reading “kinder on the eyes” and leave it at that. Plenty of people, including many with astigmatism, find light text on a dark background harder to read as the letterforms appear to bloom and blur, or even shift. There simply is no universally better mode we could predict or assume. The only sound answer is to offer both properly and let people decide for themselves ~ which is exactly why the OS preference, and the switch on top of it, both matter so much.

Which brings me to the first and most important lesson.

Lesson 1: plan for two view modes from the very start.

Everything else comes after this vital planning stage. When you build with two themes in mind from the beginning, adding the second one is almost free and certainly very easy. When you don’t consider the other side, you end up hunting down every assumption you made, every detail you did not think of without realising ~ and there are always more than you think. Something that made my work on the second theme so much harder than it needed to be :-\

Here’s a distinction worth thinking about as it is the exact thing that made my work on the view modes so much more difficult, complex and time-consuming. For the handbook I did use CSS custom properties. What it didn’t use was tokens. That sounds like the same thing, and it isn’t:

Variables name a colour within the colour scheme.
A variable like --slate-dark records what the colour is ~ one fixed value, referred to by its name, no hint to the role it plays.

Tokens name a colour’s job within the overall design.
A token like --colour-text records what the colour does ~ the styling of body text, whatever actual colour that needs to be.

Only the second kind can change between view modes, because it is the one that is defined by its role rather than its value.

My initial CSS was full of lines like color: var(--slate-dark). Clear, readable and giving the colour values names ~ yet, of course, completely stuck: “slate-dark” is only ever the one hex value. Nothing prepared for a dark-theme version. If I had written color: var(--colour-text) from the start, the very same rule could have easily been the ‘switch’ for a second theme. The variables were not wrong per se, they were just named too literally, too specifically tied to their values. This is one habit that shows the difference between a half-hour job and a long afternoon of battling code.

And it is so beautifully simple in essence: the mechanism that makes two themes painless is a set of tokens named by role: custom properties for every colour’s job, held in two sets ~ swapped by a single attribute on the root element. This is a wonderful example how you can so easily design in code (◠‿◠)
When writing the CSS, don’t reach for a raw colour value, nor for a colour-named variable ~ opt for a role token, and let the token do all the work.

/*	===========================================================
	colour tokens
	=========================================================== */

/* --- light theme (default) --- */

:root,
[data-theme="light"] {
	--colour-bg:      #f6f6f4;
	--colour-surface: #ffffff;
	--colour-text:    #1a1a1a;
	--colour-muted:   #5a5a5a;
	--colour-accent:  #4a6da7;
	--colour-border:  #d8d8d4;
}

/* --- dark theme --- */

[data-theme="dark"] {
	--colour-bg:      #16181c;
	--colour-surface: #202329;
	--colour-text:    #e8e8e6;
	--colour-muted:   #a4a4a0;
	--colour-accent:  #8fb0e8;
	--colour-border:  #33373f;
}

From then on, the rest of your CSS never mentions a colour by name:

body {
	background: var(--colour-bg);
	color:      var(--colour-text);
}

The redesigns I planned this way took the second theme in their stride. The handbook, built before I thought in this way, used colour-named variables in hundreds of rules… a nightmare to behold! Retrofitting meant visiting every one and deciding which role token it should have been all along ~ was this slate the text, a border, a heading, a surface?

My advice to you and my future self: learn from my long, drawn-out hours of code wrangling and always name colours by their job from the start, and tokenise before you theme.

Lesson 2: respect the visitor’s choice on the very first visit.

Many, if not all, of us have configured our devices and browser to suit us. The kindest thing we can do as designers is listen without being asked. That means honouring the OS and browser preference ~ not asking anyone for anything, no need to select an option or choose a setting before browsing our sites.

The prefers-color-scheme media query is how the browser passes that preference on. Map it onto the same tokens, so a visitor who prefers dark gets dark straight away:

/*	===========================================================
	respect the OS preference on first load
	=========================================================== */

@media (prefers-color-scheme: dark) {
	:root:not([data-theme]) {
		--colour-bg:      #16181c;
		--colour-surface: #202329;
		--colour-text:    #e8e8e6;
		--colour-muted:   #a4a4a0;
		--colour-accent:  #8fb0e8;
		--colour-border:  #33373f;
	}
}

The :not([data-theme]) part is very important here. It means the OS/browser preference only applies when the visitor had not set their own preference. The moment they flick the switch, data-theme appears on the root and takes precedence ~ of course, their decision and preference outranks the system default.

Lesson 3: give people a switch, and let it rule all.

Respecting the OS preference is the polite default, but everyone should still be able to override any set view mode if they fancy. Someone might keep their device in light mode all day but choose to read in dark mode for long pieces of text.

For the theme switchers I add to projects, I like to use the standard moon crescent and the age-old symbol for the sun/light of a dot in a circle. In addition, I always add a visible text label. This will make the switch action clear and accessible rather than a mystery :P

<label class="switch-btn" for="mode-toggle" aria-label="Toggle colour theme">
	<span class="switch-icon" aria-hidden="true">
		<svg class="icon-to-dark" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
			<path d="m179.378 50.111c24.06-9.048 50.123-13.998 77.333-13.998 121.42 0 220.002 98.578 220.002 220 0 121.42-98.582 219.998-220.002 219.998-121.421 0-220-98.578-220-219.999 0-6.217.258-12.373.764-18.444 8.277 88.352 82.725 157.588 173.217 157.588 96.022 0 173.98-77.959 173.98-173.98 0-96.023-77.958-173.981-173.98-173.981-10.687 0-21.15.966-31.314 2.816z" fill="currentColor"/>
		</svg>
		<svg class="icon-to-light" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
			<path d="m256 36c-121.421 0-220 98.579-220 220 0 121.42 98.579 220 220 220 121.42 0 220-98.58 220-220 0-121.421-98.58-220-220-220zm0 19.163c110.845 0 200.837 89.992 200.837 200.837s-89.992 200.837-200.837 200.837-200.837-89.992-200.837-200.837 89.992-200.837 200.837-200.837z" fill="currentColor"/>
			<circle cx="256" cy="256" r="80" fill="currentColor"/>
		</svg>
	</span>
	<span class="switch-text">
		<span class="to-dark-label">Switch to dark mode</span>
		<span class="to-light-label">Switch to light mode</span>
	</span>
</label>

A little sprinkle of vanilla JavaScript executes the mode switch, flips the attribute and remembers the choice for next time. This is the one situation where I’ll happily reach for JS, because persisting a preference between visits needs it:

/*	toggle the theme and remember it	*/

const root   = document.documentElement;
const toggle = document.getElementById( "mode-toggle" );

/* restore a saved choice on load, if there is one */
const saved = localStorage.getItem( "theme" );
if ( saved ) {
	root.setAttribute( "data-theme", saved );
}

toggle.addEventListener( "change", () => {
	const next = root.getAttribute( "data-theme" ) === "dark" ? "light" : "dark";
	root.setAttribute( "data-theme", next );
	localStorage.setItem( "theme", next );
} );

These three lessons are the entire mechanism for a nicely themed presentation of the design in light and dark modes: tokens hold the colours, the media query sets the default, and the switch allows the visitor to change the mode as they wish. If you plan for all three from the start, you’re done.
The trouble only begins when your content was built before any of this existed ~ or when you forgot to consider this from the start, grumbling at myself…

Lesson 4: your images’ flaws creep in secretly.

This was the aspect of the handbook that drove me insane, and the part I most want to remember for any future work and to warn you about. Honestly, this is painful when you forgot to plan ahead and have to go into an old project to address ~ hunting down the original files or spend ages to address via CSS updates.

When you build for a single light theme, your images quietly rely on it. A screenshot with a white margin sits nicely on a white page. A graphic saved as a transparent PNG, drawn in dark ink, reads perfectly fine against a pale background. A composited thumbnail with soft transparent edges blends nicely into the surrounding page. None of these ever look like a problem, because on a light background they are perfectly fine, not causing any visible issue at all.

Switch the view mode to a dark background and all these details surface. And you cringe at the atrocity your pages now present. The transparent thumbnail now floats with edges cut off where you expected them to fade. The dark-ink logo all but vanishes. The screenshot suddenly wears a glaring white frame that was not part of your design. None of this was visible, because at build time nothing was visibly wrong.

Even though the fix is rarely to re-export every asset ~ the time needed to check on every single detail is still torture… Quite often, the solution will be to assess whether you do need to update the original file or whether the context should actually keep a light backdrop even in the different view modes. If so, a set background then means no more need to worry.

/*	===========================================================
	give transparent media a stable backdrop
	=========================================================== */

.thumbnail img,
figure.screenshot img {
	background: #ffffff;
	padding: 0.5em;
	border-radius: 4px;
}

This works well for my handbook’s tutorials with screenshots or wireframes which are usually presented and viewed on a white background and therefore best kept this way across different view modes. This will not work if the image is an illustration, for example.

Where a mark truly needs to change between themes, a targeted swap works well:

/*	swap logos for different view modes	*/

.site-logo {
	content: url( "logo-dark-ink.svg" );
}

[data-theme="dark"] .site-logo {
	content: url( "logo-light-ink.svg" );
}

This image problem is not really the issue and it does not have a single code snippet to solve it either. The lesson learnt is more about the fact that a light-only build hides its small flaws inside its assets without any visible sign. The dark mode is merely what drags these into the daylight ~ a painful reckoning…

For my handbook, this meant that every screenshot, every thumbnail, every transparent PNG had to be reviewed. I had to look through all pages, check all visual elements and ask the same question: does this still work well on a dark background? If I had planned for two themes from the start ~ I would have only needed to ask that question once.
Retrofit, and you ask it hundreds of times over, after the fact.

Lesson 5: mind the details you didn’t create yourself.

The last detail to check is anything on the page you didn’t style yourself: embedded content, third-party widgets, syntax-highlighted code blocks, form controls. These carry their own colours and are utterly indifferent to your tokens.

For your own controls, tell the browser both themes are supported so native widgets ~ form fields, scrollbars, focus rings ~ match the mood:

/*	opt every native control into both themes	*/

:root {
	color-scheme: light dark;
}

For embedded pieces you don’t control, a container background is once again your friend, giving them a consistent backdrop, no matter which view mode is in use. And do check your focus states in both themes, not just the one you built in ~ an outline that reads beautifully on a pale background can disappear entirely on a dark one. This is exactly why I tend to set focus outlines to the accent token rather than a fixed colour, so they travel with the theme:

a:focus-visible,
button:focus-visible,
.switch-btn:focus-visible {
	outline: 1px solid var(--colour-accent);
	outline-offset: 2px;
}

Closing thoughts

If there’s a single thread running through all of this, it is that a second theme is so very easy and quick when you plan for it and surprisingly cumbersome when you don’t. The tokens, the media query and the switch are the easy and fun part ~ making the design and build of a new site run smoothly. The nightmare creeps in when something assumed a light background and the variables named their values too literally ~ all will come to haunt you, cause headaches and cost a LOT of time.

The programme hub, planned for two view modes from the start, was a breeze. The handbook, retrofitted three years on, needed every image and half its CSS coaxed into behaving. Same feature, hugely different effort ~ and the crux of it all was not planning ahead.

And I would now revisit my first, considered choice. The book metaphor was a good one and I do stand by it. However, a printed page can only ever be one thing, whereas a web page can become whatever its reader needs. That is our medium’s gift ~ a dynamic ability to change its appearance to suit. My note to self, and the note to you: consider the flexibility and benefit to all, plus the creative possibilities, light and dark modes are bringing to your project and include them from the very beginning of your design work (◠‿◠)

Links & references