Should websites have a light/dark toggle at all? Why lock a page into one experience and force a choice on the user? You decide!
A while ago, Lea Verou argued that in a website, a dark mode toggle only needs two states: light and dark, and a "system" preference.
Many other experts wanted to give their take on the argument. Vale found it "a tad too magical" to have system as an additional option, and proposed that the system would set the colour scheme from the get-go. Bramus was in the 3 button side of the debate, and didn't "... believe that being able to force a site into a specific mode should be dependent on the time of the day." On the other side, Marcin Wichary was against the 3-state side, and Josh Collinsworth reiterated that Lea got it right the first time.
This ran for weeks. And it was refreshing to see how professionals and peers had their own educated opinion. Lea Verou continued on a subsequent article, positing that the best toggle is probably none. If it's deemed important to provide a toggle to the user, then it should be located within the settings. She sent her article to a colleague with an HCI PhD for review. After a back and forth regarding if a dark mode toggle was even commonplace in websites, on her searching she noticed that every persistent toggle found was on sites built for developers.
I could not think of a single well-known consumer-facing site with a persistent dark mode toggle.
Lea Verou
Other professionals had fun takes.
As developers we tend to fall into a false consensus, thinking we're the target audience of the products we make. I myself built that toggle more than once, on smaller projects. Where it's likely I have a captive target audience of 1 😅.
So don't ask
Colour is a design decision, there's a reason why colour schemes are the way they are. There is room to respect the user's decision of light and dark mode, if the theme allows for it, but it shouldn't take the centre stage.
In nearly every project I’ve worked on, the design goes through an iterative process of experimentation and user testing. Since CSS variables became baseline in all major browsers, it's been easier to define brand rules, colour variations and beyond.
<div class="test-element">
My padding, margin, shadows, background and text colour, are defined by CSS variables.
</div>
:root {
--bg: #222;
--color: hsl(158 100% 60%);
--padding: 42px;
--width: calc(100% - 4rem);
--margin: 2rem;
--border-radius: 10px;
--shadow: 0 0 20px -3px var(--color);
}
.test-element {
background-color: var(--bg);
border-radius: var(--border-radius);
box-shadow: var(--shadow);
color: var(--color);
padding: var(--padding);
width: var(--width);
margin: var(--margin);
}
The rendered result is in the original post.
But what if colour also becomes part of the iterative process? What if we don't limit to a light and a dark and a "Green, actually" theme?
And now for something completely different!
If you've been reading up to this point, we salute you! This was all a ruse so I could talk about themes. Thinking about how colour compliment each other in a similar layout is fun! On my site you may find a vast array of themes throughout all pages and articles. The blog archive showcases what colour scheme each article might use, and I have a hidden page to help me pick from those I had already made.
But how does it work, making themes granular to the point that it can be set on a page item level?
My process works more or less like the :root {} pseudo-class, but I scope it into an element attribute. A data attribute in fact! I named it data-theme.
<div class="demo">
<article data-theme="paper">
<h4>Subtitle</h4>
<h3>Title</h3>
<p>Lorem ipsum dolor sit amet.</p>
</article>
<article data-theme="midnight">
<h4>Subtitle</h4>
<h3>Title</h3>
<p>Lorem ipsum dolor sit amet.</p>
</article>
<article data-theme="citrus">
<h4>Subtitle</h4>
<h3>Title</h3>
<p>Lorem ipsum dolor sit amet.</p>
</article>
</div>
.demo {
display: flex;
flex-wrap: wrap;
gap: 14px;
padding: 24px 18px;
width: 100%;
[data-theme='paper'] {
--surface: hsl(38 44% 97%);
--text: hsl(25 22% 20%);
--accent: hsl(8 72% 46%);
--font: Georgia, 'Times New Roman', serif;
--radius: 2px;
--border: 1px solid hsl(30 20% 82%);
--shadow: 0 1px 2px hsl(25 30% 20% / 0.1), 0 10px 28px hsl(25 30% 20% / 0.08);
}
[data-theme='midnight'] {
--surface: hsl(230 28% 11%);
--text: hsl(210 24% 92%);
--accent: hsl(158 72% 56%);
--font: ui-monospace, 'SF Mono', Menlo, monospace;
--radius: 12px;
--border: 1px solid hsl(158 72% 56% / 0.35);
--shadow: 0 0 30px hsl(158 72% 56% / 0.22);
}
[data-theme='citrus'] {
--surface: hsl(56 78% 91%);
--text: hsl(78 42% 18%);
--accent: hsl(292 60% 44%);
--font: system-ui, 'Segoe UI', sans-serif;
--radius: 18px;
--border: 0;
--shadow: 3px 3px 0 hsl(292 60% 44%);
}
[data-theme] {
--accent-alpha: hsl(from var(--accent) h s l / 35%);
}
article {
flex: 1 1 150px;
padding: 16px 18px;
background: var(--surface);
color: var(--text);
font-family: var(--font);
border: var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
}
h4 {
font-size: 12px;
font-weight: 600;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--accent);
margin: 0.5rem 0;
}
h3 {
font-size: 30px;
line-height: 1.15;
color: var(--accent);
margin: 4px 0 12px;
}
p {
font-size: 13px;
line-height: 1.7;
padding: 7px 9px;
background: var(--accent-alpha);
border-radius: calc(var(--radius) / 2);
}
p {
margin: 0;
}
}
The three cards render side by side in the original post.
The markup can be exactly the same, and the data-theme derives the differences.
I'm not claiming this a novel idea, and I'm not the only one talking about it. For example, Una Kravets called this a microtheme. Even though her article veers more on using css to adapt colours based on backgrounds, it follows the same spirit.
Rolling for colours
Nearly every colour scheme I created was generated from images or randomly using my colour palette tool. The page listens for an image paste anywhere on it, which is helpful when i find an image that translates the exact colours I'm looking for. I set the output for HSL, because it makes it easier (at least in my methods), in case I want to nudge a lightness manually without complicating with calculations.
Then I check the maths
HSL is a handy colour format to easily adapt colours, but not so great to analyse between them. the lightness value is the average of the brightest and dimmest channel, so it treats yellow and blue as equally bright. hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) both have 50% lightness, and on white the yellow has a WCAG 2 score of 1.07:1 while the blue scores 8.59:1.
WCAG 2 measures relative luminance, and it calculates it on sRGB channels. Each channel is divided by 255, and is weighted by how bright it looks, relatively. It runs from 1:1 to 21:1, and AA requires 4.5:1 for text versus background.
const relativeLuminance = ([red, green, blue]) => {
const linear = [red, green, blue].map((channel) => {
const value = channel / 255;
return value <= 0.03928 ? value / 12.92 : Math.pow((value + 0.055) / 1.055, 2.4);
});
return 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];
};
const contrast = (txt, bg) => {
const txtLuminance = relativeLuminance(txt);
const bgLuminance = relativeLuminance(bg);
return (Math.max(txtLuminance, bgLuminance) + 0.05)
/ (Math.min(txtLuminance, bgLuminance) + 0.05);
};
WCAG 2 is considered the legal minimum, required in many professional fields. In the near future, APCA will probably model better, but as of April 2026 the WCAG 3 draft contrast algorithm is yet to be determined, so for now WCAG 2.2 is the official recommendation.
One last thing
With light-dark(), each colour have a light value and a dark value, depending on the colour scheme. Set color-scheme: light dark first, then use light-dark() inside the existing custom properties. The function has been Baseline for a while, expected to be widely available in november 2026. Each microtheme gets two modes, while the themes keep their identity.
And nother thing, some high contrast accessibility settings in operating systems may push for Forced colors mode, which may affect any theme negatively. In this situation it's best to test for good ol' graceful degradation ❤️
Making things readable and accessible, equitable designs, does not remove the fun of the craft. Some of us shine like diamonds, working within constraints. Let's add colour and style the world our own way, and keep toggles to a minimum!
Originally published at jlopes.eu.


Top comments (1)
Good article😁
Don't forget the theme for high contrast mode😉