Codeskill

Learn to code, step by step

User preferences – reduced motion and colour scheme

Respecting user preferences in CSS: prefers-reduced-motion, prefers-color-scheme, and related media features. System settings exist for a reason. Your styles should not fight them.

prefers-reduced-motion

Some users disable animations at the OS level because motion causes discomfort or distraction. Query it and provide a calmer experience:

@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
  }

  .card:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgb(0 0 0 / 0.1);
  }
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

The global reset in the reduce block is blunt but effective. A finer approach disables only decorative motion and keeps essential feedback (a short opacity fade on a modal backdrop, for example).

What counts as essential motion

Loading spinners and progress indicators may still need animation. Prefer opacity pulsing over spinning when reduced motion is on, or show static text (‘Loading…’). Test with reduced motion enabled in macOS, Windows, or Firefox DevTools emulation.

prefers-color-scheme

Detect light or dark system theme and swap tokens:

:root {
  color-scheme: light dark;
  --color-text: #1a1a1a;
  --color-surface: #ffffff;
  --color-muted: #6b7280;
  --color-accent: #0366d6;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-text: #f3f4f6;
    --color-surface: #111827;
    --color-muted: #9ca3af;
    --color-accent: #60a5fa;
  }
}

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

color-scheme: light dark tells the browser to use appropriate native form controls and scrollbars where supported. Components using tokens update automatically.

Manual theme override

Many sites offer a theme toggle in addition to system preference. Use a data attribute that wins over the media query:

[data-theme="light"] {
  --color-text: #1a1a1a;
  --color-surface: #ffffff;
  --color-accent: #0366d6;
}

[data-theme="dark"] {
  --color-text: #f3f4f6;
  --color-surface: #111827;
  --color-accent: #60a5fa;
}

/* System preference only when no manual choice */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --color-text: #f3f4f6;
    --color-surface: #111827;
    --color-accent: #60a5fa;
  }
}

Store the user’s choice in localStorage if you add a toggle with JavaScript. Respect it on return visits.

Contrast in both themes

Dark mode is not just inverted colours. Light grey text on white fails; light grey on dark grey can fail too. Check contrast ratios for text, links, and focus rings in both themes. WCAG AA is 4.5:1 for normal text, 3:1 for large text and UI components.

a {
  color: var(--color-accent);
}

a:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

@media (prefers-color-scheme: dark) {
  a:visited {
    color: #a78bfa;
  }
}

Other preference media features

Less common but worth knowing:

  • prefers-contrast: more – thicken borders, avoid faint grey-on-grey
  • prefers-contrast: less – rare; soften harsh contrast if you target it
  • forced-colors: active – Windows High Contrast mode; avoid hard-coding colours that break system palette
@media (forced-colors: active) {
  .button {
    border: 2px solid ButtonText;
  }

  .button:focus-visible {
    outline: 2px solid Highlight;
  }
}

Testing checklist

  • Toggle reduced motion in OS settings and reload
  • Switch system light/dark and check tokens propagate
  • Tab through forms in both themes – focus visible?
  • DevTools rendering panel can emulate these without rebooting

The next tutorial covers CSS architecture – BEM, ITCSS, and utility hybrids without treating any of them as a religion.

PreviousForms and UI states