prefers-reduced-motion, prefers-color-scheme & prefers-contrast

User preference media queries adapt your UI to what each user actually needs — not just what device they're on. Here's a deep dive into all three with practical implementation patterns.

U

UIXplor Team

July 10, 2026 · 9 min read

01Designing for Preference, Not Just Viewport

Responsive design started with viewport adaptation — making layouts work at different screen sizes. The next phase is preference adaptation — making UIs respond to what individual users need and prefer.

The browser is a proxy for the user's operating system preferences. When a user enables Dark Mode on their Mac, Safari reports `prefers-color-scheme: dark`. When a user enables 'Reduce Motion' in iOS Accessibility settings, Safari reports `prefers-reduced-motion: reduce`.

These aren't edge cases. In 2025, dark mode usage is estimated at 60%+ among developer audiences. Reduced motion affects roughly 35% of users with vestibular disorders who explicitly enable it in their OS settings.

02prefers-reduced-motion: The Full Playbook

css
/* ✅ Correct: strip all animations */
@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 `!important` override is intentional here — it's a nuclear reset for accessibility. Some teams prefer a more surgical approach:

css
/* Targeted: only disable specific animations */
@media (prefers-reduced-motion: reduce) {
  .hero-animation { animation: none; }
  .parallax { transform: none !important; }
  .transition-all { transition: none; }
}

For Framer Motion or React Spring users:

javascript
import { useReducedMotion } from 'framer-motion';

function AnimatedCard() {
  const shouldReduce = useReducedMotion();
  return (
    <motion.div
      animate={{ x: shouldReduce ? 0 : 100, opacity: 1 }}
      transition={{ duration: shouldReduce ? 0 : 0.4 }}
    />
  );
}

03prefers-color-scheme: The Modern Implementation

The naive approach — two sets of CSS variables with a media query — works but is hard to maintain. The modern pattern uses a single token system:

css
:root {
  /* Semantic tokens map to primitives */
  --color-surface: #ffffff;
  --color-surface-elevated: #f8f9fa;
  --color-text: #0a0a0a;
  --color-text-muted: #6b7280;
  --color-border: rgba(0, 0, 0, 0.1);
  --color-accent: #6366f1;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-surface: #0a0a0f;
    --color-surface-elevated: rgba(255, 255, 255, 0.04);
    --color-text: #f9fafb;
    --color-text-muted: #9ca3af;
    --color-border: rgba(255, 255, 255, 0.08);
    --color-accent: #818cf8;
  }
}

/* All components use semantic tokens */
.card {
  background: var(--color-surface-elevated);
  border: 1px solid var(--color-border);
  color: var(--color-text);
}

Addition: support manual override from a theme toggle:

css
/* Override with data-theme attribute (set by JavaScript) */
[data-theme="light"] { /* light token values */ }
[data-theme="dark"]  { /* dark token values */ }

04prefers-contrast: Often Forgotten

css
/* Standard: pleasant, low-contrast aesthetics */
.card {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  color: rgba(255, 255, 255, 0.7);
}

/* High contrast: more opaque, more defined borders */
@media (prefers-contrast: more) {
  .card {
    background: #1a1a2e;
    border: 2px solid rgba(255, 255, 255, 0.5);
    color: #ffffff;
  }

  /* Ensure interactive elements are clearly distinguishable */
  button, a, [role="button"] {
    outline: 2px solid currentColor;
    outline-offset: 2px;
  }
}

The `forced-colors` query is the complement — it detects Windows High Contrast Mode where the OS overrides all CSS colors with system colors. Test with High Contrast Mode enabled:

05Detecting Preferences in JavaScript

javascript
// One-time check
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const prefersHighContrast = window.matchMedia('(prefers-contrast: more)').matches;

// Reactive — updates when the user changes their system preference
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (e.matches) {
    document.documentElement.setAttribute('data-theme', 'dark');
  } else {
    document.documentElement.setAttribute('data-theme', 'light');
  }
});

06Testing Your Implementation

Chrome DevTools: Rendering panel → Emulate CSS media feature. You can toggle `prefers-color-scheme`, `prefers-reduced-motion`, and `forced-colors` without changing OS settings.

macOS: System Preferences → Accessibility → Display → 'Reduce Motion' to test reduced-motion. System Preferences → Appearance → Dark Mode for color scheme.

iOS: Settings → Accessibility → Motion → Reduce Motion.

07FAQ

Should I always default to no animations and let users opt in? No. The opposite: animate by default, provide a reduced-motion alternative. Most users benefit from well-designed motion. The accessibility requirement is about allowing users to reduce motion when they need to — not eliminating it.

What percentage of users use dark mode? Data varies widely by audience. Developer-facing tools see 60–75% dark mode usage. Consumer products average 30–50%. Always test both modes.

Does `prefers-reduced-motion` affect SVG animations? Yes — SVG CSS animations and SMIL animations both respect the media query if you apply the appropriate CSS reset.

Can `prefers-color-scheme` be set programmatically? Not directly — it reads the OS preference. You override it by applying a class or data attribute to `<html>` and matching that attribute in your CSS with higher specificity.