Color Scheming

Light & Dark Modes in Modern CSS

Color-Scheming

Sara in cute cartoon avatar form, drawn by Andy Carolan Sara in cute cartoon avatar form, drawn by Andy Carolan Sara in cute cartoon avatar form, drawn by Andy Carolan

Color-Scheming

Building Light & Dark Modes in Modern CSS

1️⃣ This isn't a fully new talk.
(I worried a little about this, but let myself be convinced to give it again.)

2️⃣ Progress in CSS is lightning fast these last few years!

3️⃣ This is a talk where almost all of it is usable now.

For those who find this to be old hat, take a nap 😴

Hello! I'm Sara. Sara in cute cartoon avatar form, drawn by Andy Carolan

I'm both:

an old hand πŸ‘Œ
(I built websites as a teen from 1999)

and a not-so-newbie 🀯
(I switched career into web dev in 2022,
and began working for pirateship.com)

(Let's parley if ye be American and ship packages! Savvy?)

A Warning 🚨

There'll be some abrupt changes between dark and light mode.

If you suffer from migraines, epilepsy, or strong astigmatism,
I'm really sorry! πŸ˜“

My goal: Make using the web more comfortable for everyone.

You may have spotted a little smiley ↗️ let's test it...

Dark Mode vs. Light Mode

We all love a good polarising argument!

There are physical reasons to prefer one over the other.

And you're also allowed to prefer light or dark themed websites, even for no good or logical reason!

Your eyes, your choice! πŸ‘€

Why Dark Mode?

Pupils constrict when coming across a bright page.

Light mode has too much contrast with dark surroundings.

Floaters may be more noticeable, distorting or blocking vision.

A recreation of how floaters look in vision. Another recreation of how floaters look in vision. Yet another recreation, including text 'People with severe floaters struggle to read text like this.'

Why Light Mode?

Pupils constrict when coming across a bright page!

It's easier to focus when your pupil is smaller.

This is why as you age, you may need brighter light to read.

Astigmatism makes light text on dark backgrounds hard to read.

Astigmatism makes light text on dark backgrounds hard to read.

This upsets people enough to make lots of annoyed simulations:

Recreation of dark mode showing the effects of strong astigmatism in one eye and in both eyes. Another recreation of astigmatism in dark mode. Another recreation of astigmatism in dark mode. Another recreation of astigmatism in dark mode. Another recreation of astigmatism in dark mode. Text 'I am the douche bag who makes people read white text on a black background.' Text 'I am the douche bag who makes people read white text on a black background.' - this time distorted.

Why Not Both?

Both!

Both.

Both is good.

*nods*

Miguel and Tulio from The Road to El Dorado waggling their eyebrows at each other.

Starting from Scratch

No styles, no problem!

However...

Building this into a mature site or design system may be tricky.

Harder still if everything already has specific colours assigned.

Keep it in mind for the next time that you start a new project 🌱

What CSS Do We Need?

color-scheme        available across browsers since Feb 2022
This one definitely! For the rest, "need" is a strong word...

We'll also look at these helpful features:

Newly, Widely, What?

These are Baseline statuses.Screenshot of the Baseline availability shown on an MDN page.

Once a web feature is implemented across all major browsers, it is labelled Newly available. Then the clock starts ticking.

After 30 months (2.5 years) it is marked Widely available.
This is not related to its global usage as seen on caniuse.com.

A coarse measure of availability, simpler than a percentage.

Default HTML Themes

color-scheme

You don't even need CSS for this!

<head>
  <meta name="color-scheme" content="light dark">
</head>
:root /* or html */ {
  color-scheme: light dark;
}

color-scheme Demo

System Colors

Hellooooooo!

 How are you?
 Are you well?

You can apply color-scheme to all elements.

But they need to be assigned both a color and background-color.

Other System Colors πŸ™„

How are you?

Yeah good, you?

Could be worse!

Nice weather, innit?

It's lovely! Storm tomorrow though.

Ooh I love a good storm!

screenshot of the same slide on FireFox, light mode screenshot of the same slide on FireFox, dark mode

prefers-color-scheme

This follows OS preference,
not color-scheme property.

How are you?
Yeah good, you?

Could be worse!
Nice weather, innit?

One-Property Color Declaration

How are you?
Yeah good, you?

Could be worse!
Nice weather, innit?

It's lovely! Storm tomorrow.
Ooh I love a good storm!

Responsive color-mix() Palette

New contrast-color() Arrival!

Look at that, it's finally arrived across browsers!

This takes a colour, and returns which has the best contrast against it, black or white.

"Best" contrast here is a little tricky, so avoid mid tones.

Using light-dark()

Instead of all that mixing we can just be specific πŸ‘ˆ

Choose exact colours for dark and light mode.

But, only colors.

...for now 😏

Coming Soon: Images in light-dark()

/* image url values */
light-dark(
  url("light-icon.png"),
  url("dark-icon.png")
);


/* linear-gradient values */
light-dark(
  linear-gradient(135deg, ghostwhite 20%, tomato),
  linear-gradient(45deg, darkslategray 20%, gold)
);Screenshot of the light-dark() browser compatabiltyScreenshot of the light-dark() browser compatabilty

Coming Soon: if(), color-scheme()

#element {
  color-scheme: light dark;
  font-weight: if(color-scheme(dark): 300; else: 400);
}
Screenshot of the if() browser compatabilty Screenshot of the color-scheme() CSSWG resolution

Contrast Concerns

We worry about making a UI high-contrast enough.

But a UI can have too much contrast, too.

Migraineurs can even suffer pain if the contrast is too high 😣

Neo-Brutalistic web design has become an issue for some of us.

Migraineurs and High Contrasts

A study tested sufferers for their aversion to contrast based on the movement of a grating pattern.

I found it interesting that "Drifting" was the worst.

Stark, monochrome websites look cool. But they may also have similarities to a black and white grating pattern.

...And as we scroll, they drift.

Grating Demo

Warning! This may be uncomfortable.

If you get migraines or have epilepsy please look down.

Increasing & Decreasing Contrast

(Safe now!)

/* increase */
@media (prefers-contrast: more) {
  :root {
    background-color: Canvas;
    color: CanvasText;
  }
}
/* decrease */
@media (prefers-contrast: less) {
  :root {
    filter: contrast(70%);
  }
}

Issues: OS vs Website Settings

Neat! But how does a user specify low contrast in their OS?

Why in Windows do forced colors = prefers-contrast: more?

Some people like to keep their OS UI in light mode,
and view dark mode web pages, or vice-versa.

(Psst, browser vendors - 'prefers' toggles in toolbars, please πŸ™)

"Honour the User" - John Alsopp 2025

Presentation slide from John Alsopp's Dao of CSS. Text: Principle 3: code without coding–Wu Wei. Stop fighting the browser. Trust the user. Delete unnecessary code. Let the system work for you. The Dao of CSS is the path of least resistance.

Oh John, some days I would love to code less!

Manual Switching

To allow true choice, we currently need to roll our own switches.

When relying on prefers-color-scheme, this is a pain.

We have to override it with JavaScript and classes, and/or
double up the declaration of CSS color custom properties.

This is easier now! πŸŽ‰ Can we do it with CSS only? πŸ€”

:has() Rocks!

Can :has() Contrast?

CSS-Only Limitations

You get limited pretty fast by avoiding JavaScript.

Using :has() is magical, but only per-page.

The preference doesn't get saved while browsing the site.

You can't simply use <buttons> if you prefer those!

So what JavaScript do we need?

Jeremy Keith's Maxim

Simple Functions

Woo-hoo, Buttons!

OK great, so now we can use buttons, but this still only works per-page, and is lost when the page is reloaded ♻️

So we need to store the preference for the user.

For this we can use sessionStorage or localStorage.

sessionStorage lasts per visit to a domain. localStorage is kept between visits until the user clears their browsing data.

Saving to localStorage

Show State in Buttons

The three buttons are neat, but what is the current mode? πŸ€”

It's not too difficult to set a class with JS on the 'active' button.

But instead let's use the existing aria-pressed attribute!

This is exactly what it's for, and has the benefit that a screen reader can also announce which option is currently active.

Using aria-pressed for Buttons

Adding Contrast Switches

In Summary: Offer Choice

Use color-scheme: light dark; for a free dark mode

Experiment with color-mix() with Canvas & CanvasText

Use light-dark() to get specific within each mode

Honour your users' colour and contrast preferences

Experiment and play :)

From 2 Choices = 6 Options

more contrast
usual contrast
less contrast
light mode
dark mode

Further Reading

Thank You!

Genuinely honoured to have been invited to speak at CSS Day!

πŸ’– Γ— Γ— Γ— πŸ™

QR code linking to the slides online.

🏠 sarajoy.dev
🦣 @sarajw@front-end.social
πŸ¦‹ @sjoy.lol
πŸ› slides.sarajoy.dev/CSS-Day-2026