Monsoon v0.2.0
Made by Alexandre Kohler Star

Dark mode

Dark mode with one line of CSS

If your page uses Monsoon semantic colors, dark mode is already done. The stylesheet follows the operating system setting, and one class or attribute forces a scheme on the whole page or on any section.

3 min readBy Alexandre Kohler

Automatic: zero lines

Monsoon listens to prefers-color-scheme. When the visitor uses a dark system theme, a number called --mode becomes 1 and every semantic token recomputes: the background goes dark, text goes light, surfaces and borders follow, and the primary color is lifted so it stays readable as text. Native form controls and scrollbars switch too, because Monsoon sets color-scheme for you.

So this card is correct in both schemes without a single dark: class:

card.html
<div class="rounded-2xl border bg-surface p-6 space-y-3">
  <h3 class="text-lg font-semibold">Invoices</h3>
  <p class="text-muted-foreground">Three are waiting for approval.</p>
  <a href="#" class="text-primary font-medium underline underline-offset-4">Review now</a>
</div>

The one line: force a scheme

To pin the page to dark regardless of the system setting, add one attribute to the root element. Either form works:

index.html
<html data-theme="dark">
<!-- or -->
<html class="dark">

Use data-theme="light" or class="light" to pin light mode, which also stops the automatic switch.

Dark sections inside a light page

The same classes work on any element, so you can build a dark band in a light page, or the other way around. The tokens are recomputed for that subtree:

section.html
<section class="dark bg-background text-foreground rounded-2xl p-8">
  <h2 class="text-2xl font-bold">Always dark</h2>
  <p class="text-muted-foreground">Surfaces, borders and text switch together.</p>
  <button class="rounded-lg bg-primary text-primary-foreground px-4 py-2">Continue</button>
</section>
Both schemes side by side

class="light"

Same markup, light tokens.

class="dark"

Same markup, dark tokens.

A theme toggle in a few lines

Monsoon itself has no JavaScript, but a toggle needs a little. Store the choice and write it to data-theme on the root element. Put the first part in the <head> so the saved theme applies before the first paint:

head.html
<script>
  // in <head>: apply a saved choice before the page renders
  const saved = localStorage.getItem('theme');
  if (saved) document.documentElement.dataset.theme = saved;
</script>
toggle.html
<button id="theme" class="rounded-lg border px-3 py-2 hover:bg-muted">Toggle theme</button>
<script>
  document.getElementById('theme').addEventListener('click', () => {
    const root = document.documentElement;
    const dark = root.dataset.theme
      ? root.dataset.theme === 'dark'
      : matchMedia('(prefers-color-scheme: dark)').matches;
    root.dataset.theme = dark ? 'light' : 'dark';
    localStorage.setItem('theme', root.dataset.theme);
  });
</script>

What happens under the hood

Most dark mode setups duplicate every color: one value for light, another for dark, each switched by a selector. Monsoon declares each semantic token once. The declaration reads a number, --mode, which is 0 in light mode and 1 in dark mode, and interpolates between two lightness values with plain arithmetic:

monsoon.css
/* light: 0.99, dark: 0.16 */
--background: oklch(from var(--seed) calc(0.99 - 0.83 * var(--mode)) min(c, 0.012) h);

Switching the scheme only changes that one number. The operating system preference sets it through a media query, while .dark, .light and data-theme set it through a selector. Because the tokens are declared again on every element that changes the mode, a dark section inside a light page computes its own values from the same formulas, and a section with its own --seed inside a dark section still gets dark tokens in its own hue.

This also means your own CSS can follow the scheme without a media query. Any rule that uses var(--surface) or var(--border) is automatically correct in both modes.

Images, shadows and other details

A few things are worth a second look when a page goes dark:

  • Shadows are less visible on dark surfaces. Separate layers with bg-surface-2 or bg-surface-3 and a border instead of relying on a large shadow.
  • Logos and illustrations with dark strokes may disappear. Swap them with a <picture> element and a prefers-color-scheme media query, or give them a light backing.
  • Pure white text on a near black background can glare. Monsoon uses a soft off white for foreground, so prefer it over text-white for body copy.
  • Charts drawn with fixed palette colors keep their colors. Pick middle steps such as 500, which read on both light and dark backgrounds.

The dark: variant for palette colors

Semantic tokens switch by themselves. Fixed palette colors do not, because bg-slate-100 is a specific color. For those, Monsoon generates a dark: variant on palette backgrounds, text and borders (all steps for the neutral hues, the middle steps for the others), on white and black, and on the opacity modifiers:

badge.html
<span class="rounded-full bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-200 px-3 py-1 text-sm">
  Draft
</span>
<div class="border border-black/10 dark:border-white/10 rounded-xl p-4">...</div>

dark: applies under .dark or [data-theme=dark], and with the system dark setting unless the root element is forced to light. Use the class reference with the "Has dark:" filter to see every class that supports it.

Frequently asked questions

Do I need dark: classes for dark mode?

Not for semantic colors such as bg-surface, text-foreground or bg-primary. They switch on their own. Use dark: only for fixed palette colors.

How do I force dark mode with CSS only?

Add class="dark" or data-theme="dark" to the html element. That is the one line.

Can one section be dark while the page is light?

Yes. Put class="dark" on the section. The tokens are recomputed for everything inside it.

Does dark mode keep accessible contrast?

Yes. Token pairs are tested at 4.5:1 or better in dark mode, and the primary color is lifted so it is readable as text on the dark background.

Try it in ten seconds

Paste this into any HTML page. No install, no build.

index.html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/monsooncss">

No recent searches

Try , , or .

Recent

Loading the search index

No results for ""

Check the spelling, or search a class name such as px-4.