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.
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
/* ✅ 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:
/* 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:
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:
: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:
/* Override with data-theme attribute (set by JavaScript) */
[data-theme="light"] { /* light token values */ }
[data-theme="dark"] { /* dark token values */ }04prefers-contrast: Often Forgotten
/* 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
// 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.
Related UI Components
Related Articles