Modern CSS Accessibility Features
1. prefers-reduced-motion Implementation
| Motion Type | Default Behavior | Reduced Motion Alternative | WCAG Reference |
|---|---|---|---|
| Page transitions | Slide/fade animations (200-400ms) | Instant or crossfade (50ms) | 2.3.3 AAA (Animation from Interactions) |
| Scroll-triggered animations | Elements slide/fade in on scroll | Elements appear immediately (opacity: 1) | 2.2.2 AA (Pause, Stop, Hide) |
| Hover effects | Transform scale/rotate | Color/opacity change only | User preference respect |
| Loading spinners | Rotating animation | Pulsing opacity or static icon with ARIA | Reduce vestibular triggers |
| Parallax effects | Multi-layer scrolling | Disable completely (single-layer scroll) | 2.3.3 AAA |
| Auto-playing video | Background video loops | Static image or paused video | 2.2.2 AA (5-second rule) |
| Carousel auto-advance | Slides change every 5s | Disable auto-advance completely | 2.2.2 AA |
Example: Comprehensive prefers-reduced-motion implementation
/* Default: animations enabled */
.slide-in {
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
/* Reduced motion: instant appearance */
@media (prefers-reduced-motion: reduce) {
.slide-in {
animation: none;
/* Maintain end state without animation */
transform: translateY(0);
opacity: 1;
}
/* Reduce all animations and transitions */
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Safe alternative: crossfade instead of slide */
@media (prefers-reduced-motion: reduce) {
.modal {
animation: fadeIn 0.15s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
}
Critical: Never use
animation: none without preserving the end state. Users with
vestibular disorders can experience nausea, dizziness, and migraines from parallax, zoom, and rotation effects.
Always test with OS setting enabled.
2. prefers-color-scheme Support
| Feature | Light Mode | Dark Mode | Contrast Requirement |
|---|---|---|---|
| Background color | #ffffff or light neutrals | #000000, #121212, #1e1e1e | Maintain 4.5:1 text contrast |
| Text color | #000000, #333333, #1a1a1a | #ffffff, #e0e0e0, #f5f5f5 | 4.5:1 for body, 3:1 for large text |
| Link color | #0066cc, #1a73e8 | #66b3ff, #8ab4f8 | 4.5:1 on background + underline |
| Border/divider | #e0e0e0, #d0d0d0 | #404040, #505050 | 3:1 against adjacent colors |
| Focus indicator | #0066cc (blue) | #66b3ff (lighter blue) | 3:1 minimum (WCAG 2.4.13) |
| Code blocks | Light syntax highlighting | Dark syntax highlighting | Each token needs 4.5:1 contrast |
| Images/logos | Default version | Inverted or alternate version | Use picture element or CSS filter |
Example: Dark mode with CSS custom properties
:root {
/* Light mode (default) */
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--accent: #0066cc;
--border: #e0e0e0;
--focus-ring: #0066cc;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-primary: #1e1e1e;
--bg-secondary: #2d2d2d;
--text-primary: #e0e0e0;
--text-secondary: #a0a0a0;
--accent: #66b3ff;
--border: #404040;
--focus-ring: #66b3ff;
}
/* Adjust images for dark mode */
img:not([src*=".svg"]) {
filter: brightness(0.9);
}
/* Invert logos that need it */
.logo {
filter: invert(1);
}
}
body {
background: var(--bg-primary);
color: var(--text-primary);
}
a {
color: var(--accent);
text-decoration: underline;
}
:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
Dark Mode Best Practices: Use CSS custom properties for easy theme switching. Test contrast
ratios in both modes. Provide manual toggle that persists user preference. Avoid pure black (#000) backgrounds
(use #121212 or #1e1e1e for better readability).
3. prefers-contrast Handling
| Preference Value | User Need | Design Adjustment | Browser Support |
|---|---|---|---|
| no-preference | Standard contrast (default) | Use standard design system colors | All modern browsers |
| more | High contrast mode (Windows HC, increased contrast setting) | Increase contrast to 7:1, thicker borders, stronger colors | Chrome 96+, Edge 96+, Safari 14.1+ |
| less | Low contrast preference (light sensitivity) | Reduce contrast slightly, softer colors | Safari 14.1+, limited elsewhere |
| custom | Custom contrast settings | Respect system colors | Future spec |
Example: Responsive contrast adjustments
/* Standard contrast (default) */
.button {
background: #0066cc;
color: #ffffff;
border: 1px solid #0052a3;
}
/* High contrast mode */
@media (prefers-contrast: more) {
.button {
background: #003d7a; /* Darker for more contrast */
color: #ffffff;
border: 2px solid #000000; /* Thicker, stronger border */
font-weight: 600;
}
/* Ensure all UI components meet 7:1 ratio */
body {
--contrast-ratio: 7;
}
/* Stronger focus indicators */
:focus-visible {
outline: 3px solid #000000;
outline-offset: 3px;
}
}
/* Low contrast mode (light sensitivity) */
@media (prefers-contrast: less) {
.button {
background: #3385d6; /* Lighter blue */
color: #f5f5f5;
border: 1px solid #5c9dd9;
}
/* Reduce harsh contrasts */
body {
background: #f8f8f8;
color: #3a3a3a;
}
}
/* Windows High Contrast Mode detection */
@media (prefers-contrast: more) and (prefers-color-scheme: dark) {
/* User is in Windows High Contrast Dark theme */
.card {
border: 2px solid ButtonText;
background: Canvas;
color: CanvasText;
}
}
Windows High Contrast Mode: When enabled, Windows forces system colors. Use
prefers-contrast: more to detect and adjust. Test with forced-colors media query. Never use
background images for critical info in high contrast mode.
4. CSS Container Queries for Accessibility
| Use Case | Accessibility Benefit | Implementation | Browser Support |
|---|---|---|---|
| Responsive text sizing | Text adapts to container width, not viewport | Scale fonts based on component size for better readability | Chrome 105+, Safari 16+, Firefox 110+ |
| Component-level zoom | Better reflow at high zoom levels | Layouts adjust independently at 200%+ zoom | Meets WCAG 1.4.10 (Reflow) |
| Touch target sizing | Increase button size in narrow containers | Ensure 44px minimum in constrained spaces | Dynamic WCAG 2.5.5 compliance |
| Reading line length | Maintain optimal 50-75 character line length | Adjust columns based on container width | Improves readability (WCAG 1.4.8 AAA) |
| Focus indicator scaling | Focus rings scale with component size | Larger focus indicators in larger containers | Better WCAG 2.4.13 compliance |
Example: Accessible container queries
/* Enable container queries */
.card-container {
container-type: inline-size;
container-name: card;
}
/* Default: narrow card */
.card {
padding: 16px;
font-size: 14px;
}
.card h2 {
font-size: 18px;
}
.card button {
min-height: 44px;
padding: 8px 16px;
}
/* Medium container: increase spacing and text */
@container card (min-width: 400px) {
.card {
padding: 24px;
font-size: 16px;
}
.card h2 {
font-size: 24px;
line-height: 1.3;
}
.card button {
min-height: 48px;
padding: 12px 24px;
font-size: 16px;
}
}
/* Large container: optimal reading layout */
@container card (min-width: 600px) {
.card {
padding: 32px;
max-width: 65ch; /* Optimal line length */
}
.card p {
font-size: 18px;
line-height: 1.6;
margin-bottom: 1.5em;
}
}
/* Container query for zoom support */
@container (max-width: 320px) {
/* When container is narrow (e.g., at 200% zoom) */
.card {
/* Stack elements vertically */
flex-direction: column;
}
.card button {
width: 100%;
}
}
Container Query Benefits: Better reflow support at high zoom levels (WCAG 1.4.10). Components
adapt to available space, not just viewport. Improves readability by maintaining optimal line lengths. Enables
truly responsive components in complex layouts.
5. CSS Focus-visible Selectors
| Selector | When Applied | Use Case | Browser Support |
|---|---|---|---|
| :focus | Element has focus (keyboard or mouse) | Basic focus state - always shown | All browsers |
| :focus-visible | Keyboard focus only (heuristic-based) | Show focus ring for keyboard, hide for mouse clicks | All modern browsers |
| :focus-within | Element or descendant has focus | Style parent when child is focused | All modern browsers |
| :focus-visible:not(:focus) | Never (invalid) | Error: these states are mutually dependent | N/A |
| :has(:focus-visible) | Descendant has keyboard focus | Style container when child has focus (alternative to :focus-within) | Chrome 105+, Safari 15.4+ |
Example: Comprehensive focus-visible implementation
/* Remove default focus outline (be careful!) */
*:focus {
outline: none;
}
/* Custom focus-visible for keyboard navigation */
*:focus-visible {
outline: 2px solid #0066cc;
outline-offset: 2px;
border-radius: 2px;
}
/* Button: show focus only for keyboard */
.button {
border: 2px solid transparent;
transition: border-color 0.15s;
}
.button:focus-visible {
border-color: #0066cc;
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.2);
}
/* Input: always show focus (user is typing) */
input,
textarea,
select {
border: 1px solid #d0d0d0;
}
input:focus,
textarea:focus,
select:focus {
outline: 2px solid #0066cc;
outline-offset: 0;
border-color: #0066cc;
}
/* Focus-within: highlight form section when any field focused */
.form-section:focus-within {
background: #f5f9ff;
border-color: #0066cc;
}
/* High contrast mode: ensure focus is visible */
@media (prefers-contrast: more) {
*:focus-visible {
outline: 3px solid currentColor;
outline-offset: 3px;
}
}
/* Dark mode focus adjustment */
@media (prefers-color-scheme: dark) {
*:focus-visible {
outline-color: #66b3ff;
}
}
Focus-visible Warning: Never use
outline: none without providing an alternative
focus indicator. Always test with keyboard navigation. WCAG 2.4.13 requires 3:1 contrast ratio for focus
indicators against adjacent colors. Some browsers may show :focus-visible for mouse clicks on form inputs.
6. Scroll Behavior and Animations
| CSS Property | Accessibility Impact | Best Practice | WCAG Reference |
|---|---|---|---|
| scroll-behavior: smooth | Can cause motion sickness if animated | Disable in prefers-reduced-motion | 2.3.3 AAA (Animation from Interactions) |
| scroll-margin-top | Prevents content from hiding under sticky headers | Set to header height for skip links and anchor navigation | 2.4.1 AA (Bypass Blocks) |
| scroll-padding | Ensures focused elements fully visible | Add padding for sticky UI when element scrolled into view | 2.4.7 AA (Focus Visible) |
| overscroll-behavior | Prevents unwanted scroll chaining | Use 'contain' for modals to prevent body scroll | User experience improvement |
| scroll-snap-type | Can trap keyboard users if not careful | Ensure keyboard can reach all snap points, provide skip option | 2.1.1 AA (Keyboard) |
| position: sticky | Can obscure content if not accounted for | Use with scroll-margin-top on target elements | 1.4.10 AA (Reflow) |
Example: Accessible scroll behavior
/* Enable smooth scrolling (with reduced motion respect) */
html {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
}
/* Sticky header with scroll compensation */
header {
position: sticky;
top: 0;
height: 60px;
background: white;
z-index: 100;
}
/* Ensure anchored content visible below sticky header */
:target {
scroll-margin-top: 80px; /* Header height + spacing */
}
/* Alternative: all headings (for skip links) */
h1, h2, h3, h4, h5, h6 {
scroll-margin-top: 80px;
}
/* Focus visibility with sticky elements */
html {
scroll-padding-top: 80px;
}
/* Accessible carousel with snap scrolling */
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
}
@media (prefers-reduced-motion: reduce) {
.carousel {
scroll-snap-type: none;
scroll-behavior: auto;
}
}
.carousel-item {
scroll-snap-align: start;
scroll-snap-stop: always;
flex-shrink: 0;
width: 100%;
}
/* Modal: prevent background scrolling */
.modal {
overscroll-behavior: contain;
}
body.modal-open {
overflow: hidden;
}
/* Smooth scroll for skip links */
.skip-link:focus {
scroll-behavior: smooth;
}
@media (prefers-reduced-motion: reduce) {
.skip-link:focus {
scroll-behavior: auto;
}
}
Scroll Accessibility Tips: Always respect
prefers-reduced-motion for smooth
scrolling. Use scroll-margin-top for all anchor targets and headings when using sticky headers.
Test keyboard navigation with scroll-snap (ensure all content reachable). Prevent modal background scroll with
overscroll-behavior: contain.
Modern CSS Accessibility Quick Reference
- Reduced Motion: Use
@media (prefers-reduced-motion: reduce)to disable/minimize animations; provide instant or crossfade alternatives to slide/parallax effects (WCAG 2.3.3) - Color Scheme: Implement dark mode with
prefers-color-scheme; maintain 4.5:1 contrast in both modes; use CSS custom properties for theming - Contrast: Support
prefers-contrast: morefor high contrast mode; increase to 7:1 ratios, thicker borders, stronger focus indicators - Container Queries: Use for better reflow at zoom levels; maintain optimal line lengths (50-75ch); ensure touch targets scale appropriately
- Focus-Visible: Show focus rings for keyboard only with
:focus-visible; maintain 3:1 contrast for focus indicators (WCAG 2.4.13) - Scroll Behavior: Set
scroll-behavior: autofor reduced motion; usescroll-margin-topfor sticky headers; ensurescroll-snapdoesn't trap keyboard users - Testing: Test with OS accessibility settings enabled; verify all media queries work; use browser DevTools to simulate preferences
- Browser Support: Most features in modern browsers (2022+); provide fallbacks or progressive enhancement for older browsers
- Key Properties: scroll-margin, scroll-padding, overscroll-behavior, container-type, focus-visible, color-scheme
- Tools: Chrome DevTools (Rendering panel), Firefox Accessibility Inspector, Safari Develop menu (prefers simulation)