# Beyond Light and Dark (mode)
> Everyone arguing about dark mode toggles assumed a theme covers the whole page. Drop that, and one attribute selector gets you as many themes as you like.

- URL: https://jlopes.eu/blog/design-tokens-beyond-dark-mode/
- Date: 28 Sept 2026
- Tags: css, design-systems, accessibility

---

import Image from "@/components/Image.astro";
import Snippet from '@/components/blog/Snippet.astro'
import Post from '@/components/blog/Post.astro'
import Abbr from '@/components/blog/Abbr.astro'

export const scopeHtml = `<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>`;

export const scopeCss = `.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;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--accent);
    margin: 0.5rem 0;
  }

  h3 {
    font-size: 30px;
    line-height: 1.15;
    margin: 4px 0 12px;
  }

  p {
    font-size: 13px;
    padding: 7px 9px;
    background: var(--accent-alpha);
    border-radius: calc(var(--radius) / 2);
  }

  p {
    margin: 0;
  }
}`;

A while ago, Lea Verou argued that in a website, [a dark mode toggle only needs two states](https://lea.verou.me/blog/2026/dark-mode-toggles/ "Lea Verou -  Dark mode toggles: two states are enough"): light and dark, and a "system" preference.

Many other experts wanted to give their take on the argument. [Vale (Declan Chidlow)](https://vale.rocks/micros/20260810-0330 "Vale -  Light/Dark/System Theme Setting Design") 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](https://www.bram.us/2026/08/18/the-case-for-tri-state-dark-mode-toggles/ "Bram.us -  The Case for Tri-State Dark Mode Toggles"), 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](https://unsung.aresluna.org/solving-a-largely-imaginary-user-goal/ "Wichary, M. (2026). Solving a largely imaginary user goal") was against the 3-state side, and [Josh Collinsworth](https://joshcollinsworth.com/blog/in-defense-of-two-state-theme-toggles "Collinsworth, J. (2026). In defense of two-state theme toggles") reiterated that Lea got it right the first time.

<Image src="/uploads/beyond-light-dark-mode-spiderman.webp" alt="An image of many spider-men pointing at each other"/>

This ran for weeks. And it was refreshing to see how professionals and peers had their own educated opinion. Lea Verou [continued](https://lea.verou.me/blog/2026/dark-mode-toggles-2/ "Lea Verou - The best dark mode toggle is probably none") 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 <Abbr title="human-computer interaction">HCI</Abbr> 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 <mark>built for developers</mark>.

> I could not think of a single well-known consumer-facing site with a persistent dark mode toggle.<br/>**Lea Verou**

[Other](https://bsky.app/profile/did:plc:tjm3za6clxgg7ian727ws65l/post/3msg7el4bhc2w "Michael Ficarra - I reject your two-state toggle and submit my own 4-state toggle") [professionals](https://bsky.app/profile/did:plc:jrfasgrfx46fby4mn5qskd4k/post/3mtnvx4l32224 "Andreas Møller - Everyone knows that the right answer is 100") [had](https://bsky.app/profile/did:plc:kzbz4qsltwkq3baxgue7ju4k/post/3mtt4cbrufs24 "T. Afif - Dark mode with spotLight") [fun](https://bsky.app/profile/did:plc:kc66te46cf4pmnkugea7fe5d/post/3mtwuswoj3s2m "Sunkanmi Fafowora -  Capitalism mode") [takes](https://bsky.app/profile/did:plc:xhhcrzsilpamjmz4dvrpt7df/post/3mtr22cpsb22u "Coiyer C. - I actually strive for perfect clarity with my color theme pickers.").

As developers we tend to fall into a [false consensus](https://www.nngroup.com/articles/false-consensus/ "Nielsen-Norman group - You Are Not the User: The False-Consensus Effect"), 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](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/--* "Custom properties (--*): CSS variables") became baseline in all major browsers, it's been easier to define brand rules, colour variations and beyond.

export const firstHTML = `<div class="test-element">
  My padding, margin, shadows, background and text colour, are defined by CSS variables.
</div>`;

export const firstCSS = `: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);
}`;

<Snippet css={firstCSS}>
  <Fragment set:html={`<style>${firstCSS}</style>${firstHTML}`} />
</Snippet>

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](/blog/) showcases what colour scheme each article might use, and I have a **<a href="/themes/" title="Themes page" target="_blank" rel="noopener">hidden page</a>** 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 {}`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:root ":root CSS pseudo-class") pseudo-class, but I scope it into an element attribute. A [data attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/How_to/Use_data_attributes "Use data attributes") in fact! I named it `data-theme`.

<Snippet html={scopeHtml} css={scopeCss}>
  <Fragment set:html={`<style>${scopeCss}</style>${scopeHtml}`} />
</Snippet>

**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](https://una.im/modern-css-theming/ "Una Kravets - Modern CSS theming with light-dark(), contrast-color(), and style queries") 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 <a href="https://jlopes.eu/tools/colour-palette" title="Colour palette generator, jlopes.eu" target="_blank" rel="noopener">my colour palette tool</a>. 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

<Abbr title="hue, saturation and lightness">HSL</Abbr> 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 <Abbr title="Web Content Accessibility Guidelines">WCAG</Abbr> 2 score of  **1.07:1** while the blue scores **8.59:1**.

WCAG 2 measures [relative luminance](https://www.w3.org/TR/WCAG22/#dfn-relative-luminance "Relative luminance. WCAG 2.2, W3C Recommendation"), and it calculates it on <Abbr title="standard red, green and blue">sRGB</Abbr> channels. Each channel is divided by 255, and is weighted by how bright it looks, relatively. It runs from [1:1 to 21:1](https://www.w3.org/TR/WCAG22/#dfn-contrast-ratio "Contrast ratio. WCAG 2.2, W3C Recommendation"), and AA requires <mark>4.5:1</mark> for text versus background.

```js
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](https://www.gov.uk/service-manual/helping-people-to-use-your-service/understanding-wcag "Understanding WCAG 2.2. GOV.UK Service Manual"), required in many professional fields. In the near future, <Abbr title="Accessible Perceptual Contrast Algorithm">APCA</Abbr> will probably model better, but as of [April 2026](https://adrianroselli.com/2026/04/wcag3-contrast-as-of-april-2026.html "Roselli, A. (2026). WCAG3 Contrast as of April 2026") the WCAG 3 draft  contrast algorithm is **yet to be determined**, so for now [WCAG 2.2](https://www.w3.org/TR/WCAG22/ "Web Content Accessibility Guidelines (WCAG) 2.2. W3C Recommendation") is the official recommendation.

## What else should we keep in mind?

Some high contrast accessibility settings in operating systems may push for [Forced colors](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors "forced-colors CSS media feature") mode, which may affect any theme negatively. In this situation it's best to test for good ol' [graceful degradation](https://www.smashingmagazine.com/2022/03/windows-high-contrast-colors-mode-css-custom-properties/ "Bailey, E. (2022). Windows High Contrast Mode, Forced Colors Mode And CSS Custom Properties. Smashing Magazine") ❤️

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!
