Thinking About Dark Mode and Accessibility Together
AI generated
A11Y
WCAG
Accessibility · Dark Mode · WCAG · Contrast
Thinking About Dark Mode and Accessibility Together
Contrast, halation, and prefers-color-scheme done right

Dark mode is often treated as a pure taste question, but it is an accessibility topic with real tension built in: it relieves light-sensitive users while potentially worsening readability for users with astigmatism through the halation effect. Teams that recalculate contrast ratios correctly, respect prefers-color-scheme, and avoid pure black build a color scheme that genuinely works for both groups.

13 min read WCAG 2.2 · prefers-color-scheme · Contrast Ratio Tailwind CSS · Hyvä Theme · Alpine.js

1. Why dark mode is not just a design topic

Dark mode often gets treated in project meetings as a pure aesthetics question: dark color scheme on or off, depending on the trend. This view overlooks that a dark color scheme has an immediate effect on readability for very different groups of users. For part of the audience, dark mode is a genuine accessibility measure that reduces glare and makes extended reading tolerable in the first place. For another part of the audience, the same switch measurably worsens character recognition, because the human eye reacts physiologically differently to bright objects on a dark background than to dark objects on a bright background.

This ambivalence has a practical consequence: a dark mode that simply inverts the colors of a light design does not solve an accessibility problem, it merely shifts it. Anyone planning dark mode and accessibility together needs to keep both user groups in mind, calculate the contrast ratio for the dark scheme independently, and let the system decide which scheme is active initially. The following sections show how this is implemented concretely in Magento and Hyvä stores, from the physiological basis to the Alpine.js component for a manual toggle.

2. Who dark mode helps: light sensitivity and migraine

For users with photophobia, chronic migraine, or certain neurological conditions, a glaring white interface acts like a direct light source in the field of view. An average screen with a white background emits noticeably more light than the same screen with a dark background, especially in dark environments or during extended evening use. For this group, dark mode does not just reduce discomfort, it measurably prevents symptom escalation: studies on migraine triggers regularly cite glaring light as one of the most common triggers.

For people with certain forms of cataract or increased light sensitivity after eye surgery, a dark color scheme is not a comfort option but a functional necessity to be able to use a page for any length of time at all. The WCAG rules themselves do not mandate a specific color scheme, but Success Criterion 1.4.8 (Visual Presentation) requires that users be able to select foreground and background colors themselves when a text block alternative is offered. prefers-color-scheme is the technical implementation of this principle at the operating system level, without every website having to build its own color picker.

3. Who dark mode hurts: astigmatism and the halation effect

For users with astigmatism, a very common vision condition caused by an irregularly curved cornea, dark mode can have the opposite effect. The so-called halation effect describes how bright characters on a dark background scatter within the eye, creating a slight glow around each letter. With strong contrast between very bright text and a very dark background, character edges blur more than they would with dark text on a light background. For people with uncorrected or partially corrected astigmatism, reading in dark mode becomes more strenuous rather than easier.

This effect is not a footnote: astigmatism affects an estimated one third of the adult population to some degree, many of them unaware of it or only lightly corrected. The practical consequence for accessibility work is clear: a forced, non-toggleable dark mode is just as problematic as a forced light mode. The solution is not choosing one scheme, but implementing both schemes cleanly, letting the operating system make the initial choice, and always offering the user a manual toggle.

4. Recalculating contrast ratios for a dark color scheme

A common mistake in implementation: developers copy the color values of the light design, invert them mechanically, and assume the contrast ratio is preserved. That is often true mathematically, because the WCAG contrast formula is based on relative luminance and reacts symmetrically to a pure inversion, but it completely ignores the halation effect from section 3. A contrast ratio of 21:1 between pure white (#ffffff) and pure black (#000000) formally passes WCAG AAA with top marks, yet it is more uncomfortable to read for many users than a slightly reduced ratio of 15:1 to 17:1 with a softened white tone.

For body text, WCAG 2.2 Success Criterion 1.4.3 requires at least 4.5:1, and for large text (24px and above, or 19px bold) at least 3:1. These minimums apply unchanged in dark mode, but the practical recommendation deliberately sits above them, in the range of 7:1 to 15:1, to leave room for halation avoidance without dropping below the minimum. Tools like the WebAIM Contrast Checker or the Chrome DevTools contrast inspector must be run separately for every color combination in the dark scheme, an automatic carryover of the light values is not sufficient.


/* Contrast ratios calculated separately for light and dark scheme,
   not simply inverted from one another */
:root {
  --color-bg: #ffffff;
  --color-text: #1c1917;      /* ratio to bg: 17.9:1 */
  --color-muted: #57534e;     /* ratio to bg: 7.1:1  */
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-bg: #18181b;      /* not pure #000000 */
    --color-text: #e4e4e7;    /* not pure #ffffff, ratio to bg: 15.8:1 */
    --color-muted: #a1a1aa;   /* ratio to bg: 7.3:1, still passes AA for large text */
  }
}

body {
  background-color: var(--color-bg);
  color: var(--color-text);
}

5. Respecting prefers-color-scheme correctly in code

The media query prefers-color-scheme reads the user's operating system setting and is the correct starting point for every dark mode concept. The key principle: the website should follow the system, not the other way around. An interface that always starts in light mode on first visit and forces the user to switch manually ignores a deliberately made system setting and creates unnecessary friction, especially for users who work consistently in dark mode for medical reasons.

In CSS-first approaches like Tailwind CSS v4, prefers-color-scheme can be mapped directly through CSS variables and the dark: variant, without JavaScript on the first render. It is important to set the initial color scheme server-side or via an inline script before the first paint, otherwise a visible flash of wrong theme occurs, where the page briefly flashes light before JavaScript switches to dark mode afterward. This flash is not just unattractive, it is a real small burden for light-sensitive users on every page visit.


<!-- Inline script in <head>, before any CSS or content paints,
     prevents a flash of the wrong color scheme on first load -->
<script>
  (function () {
    var stored = localStorage.getItem('color-scheme');
    var systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    var isDark = stored ? stored === 'dark' : systemDark;
    document.documentElement.classList.toggle('dark', isDark);
  })();
</script>

/* Tailwind CSS v4 CSS-first dark mode setup */
@import "tailwindcss";

@custom-variant dark (&:where(.dark, .dark *));

/* Falls back to the OS setting when no class is set manually */
@media (prefers-color-scheme: dark) {
  :root:not(.light) {
    color-scheme: dark;
  }
}

6. Why pure black with pure white is usually a mistake

The practical takeaway from section 3 translates into a simple rule: #000000 as background and #ffffff as text should be avoided in a dark mode color scheme, even though this combination delivers the highest possible contrast ratio. The reason lies in the physiology of the eye, not in the math of the contrast formula: OLED screens with true black produce a particularly strong brightness jump at every character edge, which amplifies the halation effect and can additionally lead to perceived flicker during eye movement.

The proven solution, also used by major systems like Material Design and the macOS system design, is a dimmed gray instead of pure black for the background (roughly #18181b to #121212) and a slightly softened white instead of pure white for the text (roughly #e4e4e7 instead of #ffffff). This combination stays comfortably above the WCAG minimums while noticeably reducing the brightness jump at character edges. For users who still need maximum contrast, for example with severe visual impairment without astigmatism, a separate high-contrast mode via prefers-contrast: more should be available, rather than tuning the default dark mode to the extreme.

7. Dark mode in Hyvä themes with Tailwind and Alpine.js

Hyvä themes already bring the right tools for a clean dark mode concept via Tailwind CSS and Alpine.js, without needing to load additional JavaScript libraries. Color definitions belong centrally in the CSS variables of the tailwind.css, not scattered across individual .phtml templates, so every component automatically supports both color schemes. For Magento-specific elements such as prices, stock status, or discount badges, colors in the dark scheme need to be checked individually, since a strong red for "out of stock" often produces a different contrast ratio on a dark background than on a light one.

For the initial state without a flash of wrong theme, Hyvä only needs a small inline script in Magento_Theme::html/head.phtml that runs before the first Tailwind stylesheet. The actual toggle logic can then be encapsulated as a standalone Alpine.js component that persists the state in localStorage and reacts to system changes via a matchMedia listener, without requiring a page reload.


// Alpine.js component for manual dark mode toggle in a Hyvä theme,
// registered globally via Alpine.data() in default.phtml
document.addEventListener('alpine:init', () => {
  Alpine.data('colorSchemeToggle', () => ({
    isDark: document.documentElement.classList.contains('dark'),

    init() {
      // React to OS-level scheme changes when the user has no manual override
      window.matchMedia('(prefers-color-scheme: dark)')
        .addEventListener('change', (event) => {
          if (!localStorage.getItem('color-scheme')) {
            this.setScheme(event.matches);
          }
        });
    },

    toggle() {
      this.setScheme(!this.isDark);
      localStorage.setItem('color-scheme', this.isDark ? 'dark' : 'light');
    },

    setScheme(dark) {
      this.isDark = dark;
      document.documentElement.classList.toggle('dark', dark);
    }
  }));
});

8. User control: toggle, persistence, and ARIA

A color scheme toggle is itself an accessibility-relevant control and must meet the same requirements as any other interactive button. It needs an accessible name via aria-label or visible text, must be focusable and triggerable by keyboard, and its current state should be exposed via aria-pressed for screen reader users. A bare icon without a label, understandable only through visual interpretation as "sun" or "moon", does not meet this requirement.

The chosen setting should persist across page visits, typically in localStorage, so the user does not have to repeat the decision on every visit. At the same time, a "system default" state should remain available that clears the manual choice and falls back to prefers-color-scheme, for users who deliberately change their system setting situationally, for example light during the day and dark in the evening via an operating system schedule.


<!-- Accessible dark mode toggle button in a Hyvä .phtml template -->
<button
    type="button"
    x-data="colorSchemeToggle"
    x-on:click="toggle()"
    x-bind:aria-pressed="isDark.toString()"
    aria-label="Toggle dark color scheme"
    class="inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-3 py-2 text-sm font-medium text-zinc-700 hover:bg-zinc-100 dark:border-zinc-600 dark:text-zinc-200 dark:hover:bg-zinc-800"
>
  <svg x-show="!isDark" class="w-4 h-4" aria-hidden="true"><!-- sun icon --></svg>
  <svg x-show="isDark" class="w-4 h-4" aria-hidden="true"><!-- moon icon --></svg>
  <span x-text="isDark ? 'Dark' : 'Light'"></span>
</button>

9. Dark mode decisions in direct comparison

Many dark mode implementations fail not on the basic idea, but on the same recurring detail decisions. The following overview contrasts typical mistake patterns with the recommended solutions, each with the concrete accessibility benefit.

Decision Common mistake Recommended approach Benefit
Background color #000000 #18181b to #121212 Less halation with astigmatism
Text color #ffffff #e4e4e7 Less glow at character edges
Initial mode Always forced light Respect prefers-color-scheme No friction for a deliberate system choice
Switch timing After first paint via JS Inline script before first paint No flash of wrong theme
Toggle button Icon without aria-label aria-label + aria-pressed Screen reader detects state

The table shows a consistent pattern: almost every mistake stems from mechanically inverting a light design instead of treating the dark scheme as an independent design system with its own contrast check. Teams that consistently implement these five points already cover the majority of practically relevant dark mode barriers before a specialized accessibility test is even needed.

Mironsoft

Accessibility, contrast audits, and dark mode concepts for Magento and Hyvä stores

Ready to implement dark mode accessibly?

We audit existing color schemes for contrast problems in light and dark mode, recalculate the contrast ratios, and implement prefers-color-scheme cleanly, including a persistent toggle with correct ARIA markup.

Contrast audit

Check contrast ratios for light and dark mode separately

Dark mode concept

Color scheme without pure black, without halation problems

Hyvä implementation

Alpine.js toggle with persistence and aria-pressed

10. Summary

Combining dark mode and accessibility solves a problem with two opposing user groups: light-sensitive users and people with migraine benefit noticeably from a dark color scheme, while users with astigmatism can read worse due to the halation effect under strong light-dark contrast. Both groups are served by respecting prefers-color-scheme as the starting point, calculating the contrast ratio for the dark scheme independently, and replacing pure black with pure white by a dimmed gray and a slightly reduced white.

Technically, for Hyvä stores this means an inline script before the first paint against the flash of wrong theme, central CSS variables instead of scattered color values across individual templates, and an Alpine.js toggle with correct ARIA markup and localStorage persistence. Teams that plan for these points from the start save later rework and deliver a color scheme that actually works for significantly more users than a simple inversion.

Dark mode and accessibility, the essentials at a glance

Two user groups

Light-sensitive users benefit from dark mode, users with astigmatism suffer from the halation effect under strong contrast.

Recalculate contrast

WCAG minimums of 4.5:1 apply in dark mode too, 7:1 to 15:1 is recommended instead of the maximum values at #000000/#ffffff.

prefers-color-scheme

Respect the system setting, use an inline script before the first paint against a flash of wrong theme.

No pure black

#18181b instead of #000000, #e4e4e7 instead of #ffffff, reduces halation without dropping below WCAG minimums.

11. FAQ: Dark Mode and Accessibility

1Is dark mode fundamentally more accessible than a light design?
No. Light-sensitive users benefit from dark mode, users with astigmatism can read worse due to halation. Offer both modes and let the system make the initial choice.
2What is the halation effect and who does it affect?
Bright characters on a dark background scatter within the eye and create a glow. Mainly affects people with astigmatism, a very common vision condition.
3Why should pure black be avoided in dark mode?
Pure black with pure white creates the strongest brightness jump at character edges and amplifies halation. A dimmed gray like #18181b is the proven alternative.
4What contrast ratio do I need under WCAG?
At least 4.5:1 for body text, 3:1 for large text, unchanged in dark mode. Recommended: 7:1 to 15:1 instead of the maximum of 21:1.
5What does prefers-color-scheme do technically?
Reads the user's operating system setting and allows the color scheme to adapt automatically, without manual switching.
6How do I prevent a flash of wrong theme?
An inline script in the head before the first CSS paint reads localStorage and prefers-color-scheme and immediately sets the matching class.
7Does a toggle button need special ARIA attributes?
Yes, aria-label or visible text for the accessible name, aria-pressed for the current state, so screen reader users can recognize the mode.
8Is a simple inversion of the light colors enough?
No, mechanical inversion ignores halation and often leads to pure black/white. The dark scheme needs its own contrast check.
9How do I implement dark mode in a Hyvä theme?
Central CSS variables in tailwind.css, inline script in head.phtml against the flash, Alpine.js component for the toggle with localStorage persistence.
10Is there a compromise for maximum contrast with halation sensitivity?
A separate high-contrast mode via prefers-contrast: more can be offered in addition, instead of tuning the standard dark mode to the extreme.