# Dark mode with faint grey text and icons

Source: https://easeweb.dev/learn/dark-theme-contrast
Topics: Colour and contrast > Dark themes keep text and icons readable (WCAG 1.4.3, 1.4.11)
The fix: Either give every colour a role token, such as muted text and icon, and redefine each one in the dark theme, or write each colour together with its dark value using light-dark(). Either way, draw the SVG icons in currentColor so they take the button's colour.
Test with: dark mode, contrast measurement, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/light-dark https://developer.mozilla.org/en-US/docs/Web/CSS/color-scheme https://hidde.blog/making-single-color-svg-icons-work-in-dark-mode/

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A recipe site has a dark theme that follows the device's setting. In the design file, the dark version was made by swapping the big colours: the page and the cards go from pale grey and white to deep slates, and the titles from near-black to near-white. The rest was carried across from the light version as it was: the grey line under each title that gives the time and the servings, and the Save and Share icons beside it. In the light theme they are quiet and clear, which is what the design wanted. The developer built exactly that, with the page, card and title colours as tokens that a `prefers-color-scheme: dark` block redefines.

Set the device to dark mode and the site follows. The titles read well. The line under them, "25 min · Serves 4", is now a mid grey on a dark card, and the Save and Share icons are dark grey on dark grey: there for someone who knows where to look, and gone for anyone who doesn't.

## Why it fails

The dark theme redefines the colours it has tokens for, and nothing else. The grey is written into `.meta` as a hex value, and the icons' colour into each SVG's `fill` attribute, so neither changes when the theme does:

- The time line, `#6b7280`, is 4.83:1 on the white card and 3.04:1 on the `#1f2937` card. WCAG 1.4.3 asks for 4.5:1 for text this size.
- The icons, `#374151`, are 10.31:1 on white and 1.42:1 on the dark card. They are all that shows what each button is, so 1.4.11 asks for 3:1 against what is around them.

Both criteria measure the colours the page shows, and the dark theme is what this page shows to everyone with dark mode on: the site's own CSS switches to it. None of it is the browser's doing. Each colour on screen is one the site set, or never set for the dark theme.

## Who is affected

A site that follows the system setting shows its dark theme to everyone with dark mode on, including phones set to switch to it at night, so many readers never chose this site's theme at all.

## What automation and AI miss

Automated checkers, axe included, measure the page in the colour scheme the browser is in, and a headless browser is in light mode unless it is told otherwise. In light mode every colour on this page passes, so the scan is clean. EaseWeb's audit emulates dark mode on pages whose colours change with it and measures them again, but only a theme that follows the system setting: one switched on only by the site's own toggle is never reached. The Before also passes a review of the dark theme's CSS, because the dark block is complete for every token it has; what is missing is in the components. A generated dark theme can make the same cut, flipping the page and the main text and leaving the greys and the icon fills on the cards as they were.

## Before: the dark theme swaps the page and the text, and nothing else

```html
<article class="recipe">
  <h2>Charred leeks with romesco</h2>
  <p class="meta">25 min · Serves 4</p>
  <button type="button" class="icon-button" aria-label="Save">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="#374151" d="…"/>
    </svg>
  </button>
  <button type="button" class="icon-button" aria-label="Share">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="#374151" d="…"/>
    </svg>
  </button>
</article>
```

```css
:root {
  --page: #f3f4f6;
  --card: #ffffff;
  --ink: #111827;
}
@media (prefers-color-scheme: dark) {
  :root {
    --page: #111827;
    --card: #1f2937;
    --ink: #f3f4f6;
  }
}
body {
  background: var(--page);
  color: var(--ink);
}
.recipe {
  background: var(--card);
}
.meta {
  color: #6b7280; /* 4.83:1 on white */
}
```

In the light theme this passes. In the dark theme:

- The time line keeps `#6b7280`, 3.04:1 on the dark card.
- The Save and Share icons keep `fill="#374151"`, 1.42:1 on the dark card.

## After: a theme token for every colour

```html
<article class="recipe">
  <h2>Charred leeks with romesco</h2>
  <p class="meta">25 min · Serves 4</p>
  <button type="button" class="icon-button" aria-label="Save">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
  <button type="button" class="icon-button" aria-label="Share">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
</article>
```

```css
:root {
  --page: #f3f4f6;
  --card: #ffffff;
  --ink: #111827;
  --muted: #6b7280; /* 4.83:1 on white */
  --icon: #374151;  /* 10.31:1 on white */
}
@media (prefers-color-scheme: dark) {
  :root {
    --page: #111827;
    --card: #1f2937;
    --ink: #f3f4f6;
    --muted: #9ca3af; /* 5.78:1 on #1f2937 */
    --icon: #d1d5db;  /* 9.96:1 on #1f2937 */
  }
}
body {
  background: var(--page);
  color: var(--ink);
}
.recipe {
  background: var(--card);
}
.meta {
  color: var(--muted);
}
.icon-button {
  color: var(--icon);
}
```

1. Add a token for each role the components use, here `--muted` for quiet text and `--icon` for icons, with the light theme's values. The light theme looks exactly as it did.
2. Redefine both in the dark block, measured on the dark card: the grey lifts to `#9ca3af`, 5.78:1, and the icons to `#d1d5db`, 9.96:1.
3. Colour `.meta` with `var(--muted)` instead of its hex value.
4. Set the SVG paths to `fill="currentColor"`, and give `.icon-button` its colour from `--icon`. The icon now takes whatever colour the button has, in either theme.

This fixes the Before and passes 1.4.3 and 1.4.11 in both themes. The dark version keeps the design's quiet grey line and soft icons, a step lighter.

## After: each colour written with its dark value, using light-dark()

```html
<article class="recipe">
  <h2>Charred leeks with romesco</h2>
  <p class="meta">25 min · Serves 4</p>
  <button type="button" class="icon-button" aria-label="Save">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
  <button type="button" class="icon-button" aria-label="Share">
    <svg viewBox="0 0 24 24" aria-hidden="true">
      <path fill="currentColor" d="…"/>
    </svg>
  </button>
</article>
```

```css
:root {
  color-scheme: light dark;
  --page: #f3f4f6;
  --card: #ffffff;
  --ink: #111827;
}
@media (prefers-color-scheme: dark) {
  :root {
    --page: #111827;
    --card: #1f2937;
    --ink: #f3f4f6;
  }
}
body {
  background: var(--page);
  color: var(--ink);
}
.recipe {
  background: var(--card);
}
.meta {
  /* 4.83:1 on white, 5.78:1 on #1f2937 */
  color: light-dark(#6b7280, #9ca3af);
}
.icon-button {
  /* 10.31:1 on white, 9.96:1 on #1f2937 */
  color: light-dark(#374151, #d1d5db);
}
```

1. Add `color-scheme: light dark` to the root. It tells the browser the page supports both schemes, so `light-dark()` follows the device's setting; without it, `light-dark()` always takes its first value.
2. Write the grey as `light-dark(#6b7280, #9ca3af)`: the light theme's value first, the dark theme's second, measured on the dark card at 5.78:1.
3. Set the SVG paths to `fill="currentColor"`, and give `.icon-button` the pair `light-dark(#374151, #d1d5db)`, 9.96:1 on the dark card.

This fixes the Before and passes 1.4.3 and 1.4.11 in both themes. `light-dark()` has worked in current versions of every major browser since May 2024. An older browser drops the whole declaration, so if older browsers matter, put `color: var(--ink);` before each pair: there, the time line and the icons show in the body's ink, less quiet than the design but readable. `color-scheme` also gives the browser's own parts, such as scrollbars and unstyled form controls, their dark look.

## Which option to use

Both pass, and both give each colour a dark value; they differ in where that value lives. Theme tokens keep every colour in one place, so a design system's palette and its ratios can be reviewed together, and a new component that uses the tokens is right in both themes without anyone thinking about it. Choose them when several components share the same greys. `light-dark()` keeps each pair on one line in the component, where a reviewer sees at once that a colour has its dark value, and it needs no second block. Choose it for a component with colours of its own, or a site with only a few. Both still need the icons in `currentColor`: no token or pair reaches a colour written into the SVG.

## Implementation decisions

Measure each colour on every surface it sits on in the dark theme, not only on the page. Dark themes often show depth with lighter surfaces, so a grey that passes on the `#111827` page can fall short on a lighter card, banner or menu, where its contrast is lowest.

A theme switched by the site's own toggle, rather than by the system setting, needs the same values. Set an attribute such as `data-theme="dark"` on the root from the toggle and redefine the tokens under that selector as well as in the media query, or set `color-scheme` on the root from the toggle so that `light-dark()` follows it. WCAG's Understanding page for 1.4.3 allows colours below the minimum that a user chooses, "provided there is a mechanism that will return to the required values"; whether a dark theme behind a toggle, with the light theme a click away, counts as such a choice is still an open question for WCAG. A theme that follows the system setting has no such click: it is the page every dark-mode user gets. Either way, some people choose dark mode because they need it to read, so measure every theme you ship.

With Tailwind, each colour utility needs its own `dark:` pair (`text-gray-500 dark:text-gray-400`), and one that is missed keeps its light value. Defining the colours as theme variables that change in the dark theme lets the utilities name roles, as the tokens above do, so there is no pair to forget.

Icons drawn as `<img>` files can't take `currentColor`: the page's CSS doesn't reach inside them. Inline single-colour icons as SVG, or supply a second file for the dark theme. A `prefers-color-scheme` query inside the SVG file itself is another way, but Safari has long ignored it in an SVG loaded by `<img>`. Text in a logo is exempt from 1.4.3, but a black wordmark on a dark page still looks broken: use `<picture>` with a `<source media="(prefers-color-scheme: dark)">` that points at a light version.

The focus ring is the same failure. A `#1a4fd0` ring is 6.83:1 on white and 2.15:1 on the `#1f2937` card; give it a token or a pair of its own. An embedded widget in an iframe keeps its own colours: use its dark option if it has one, or leave it light, which passes as long as its own colours do.

## Verify the fix

1. Set the device, or the browser's emulation of `prefers-color-scheme`, to dark, reload, and measure every text colour against the surface directly behind it: at least 4.5:1, or 3:1 for large text.
2. In dark mode, measure every icon-only control and every field edge against the colour around it: at least 3:1.
3. Search the components' CSS and SVG for colours that are neither a theme token nor a `light-dark()` pair (`#`, `rgb(`, `fill="`, `stroke="`), and give each one its dark value.
4. Look at every logo, illustration, chart and embedded widget in dark mode, and confirm none has become a dark shape on the dark page.
5. Switch back to light mode, and confirm every ratio from the light theme is unchanged.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the theme tokens or shared component styles change.

## Limits

This is a starting pattern, not a guarantee for every application. Colour values and token names are examples; measure your own palette on your real surfaces. The ratios for the light theme are covered in [low-contrast text on brand colours, photos and hover states](/learn/text-contrast) and [low-contrast form fields, icons and focus rings](/learn/non-text-contrast). A Windows contrast theme replaces the site's colours with the user's own, which is a different failure, covered in [high contrast mode hiding focus, icons and selections](/learn/forced-colors). Check the complete pages, in both themes, with people who rely on them.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
