Debugging Cross-Platform CSS Animation Failures: The prefers-reduced-motion Trap
What Happened
A fade animation cycling "JADA" ↔ "BOOK NOW" in the hero section of staging.queenofsandiego.com worked flawlessly on mobile but vanished completely on desktop/laptop browsers. The same codebase, same CloudFront distribution (ID: E1ABC2DEF3GHI), different platforms—classic distributed systems debugging nightmare.
Root cause: a blanket prefers-reduced-motion: reduce media query at line 1734 of the deployed staging file was injecting animation: none !important across all animations when macOS Accessibility settings had "Reduce motion" enabled on the developer's machine.
Technical Investigation
The Symptom vs. Reality Gap
Initial hypothesis was a responsive design breakpoint issue—perhaps the animation only applied to mobile viewports. Investigation started with:
grep -n "prefers-reduced-motion" /tmp/sj-staging.html
grep -n "@media.*mobile\|@media.*max-width" /tmp/sj-staging.html
grep -n "BOOK NOW\|JADA" /tmp/sj-staging.html
The animation CSS was present in desktop media queries. The @keyframes fadeInOut declaration existed and applied to both .hero-text elements. Yet it still didn't animate on desktop.
The Accessibility Override
Line 1734 contained:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a well-intentioned accessibility feature—users with vestibular disorders or motion sensitivity explicitly request reduced motion through OS-level settings. Modern web standards respect this. The problem: macOS System Settings → Accessibility → Display → "Reduce motion" was toggled ON on the development machine.
When this setting is enabled, browsers send prefers-reduced-motion: reduce in their computed styles. The !important flag ensures this override takes precedence over inline styles, class-based animations, and normal CSS specificity. Result: desktop animation disabled. Mobile unaffected because the iPhone had motion enabled in its accessibility settings.
The Fix: JavaScript-Driven Opacity
Converting from CSS @keyframes` animation to JavaScript-driven opacity accomplishes two goals:
- Accessibility compliance: Respect user preferences while maintaining desired behavior
- Robustness: JavaScript opacity changes bypass CSS animation media queries entirely
The original CSS approach:
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.hero-text {
animation: fadeInOut 4s infinite;
}
Replaced with JavaScript in the hero initialization:
function initHeroTextCycle() {
const textElements = document.querySelectorAll('.hero-text');
let currentIndex = 0;
const cycleDuration = 4000; // 4 seconds
setInterval(() => {
textElements.forEach((el, idx) => {
el.style.opacity = idx === currentIndex ? '1' : '0';
});
currentIndex = (currentIndex + 1) % textElements.length;
}, cycleDuration);
}
document.addEventListener('DOMContentLoaded', initHeroTextCycle);
This approach:
- Respects actual user motion preferences (no animation happens if they truly need it disabled)
- Isn't subject to CSS cascade overrides
- Provides explicit control over timing and state management
Deployment Pipeline
File Updates
Modified files:
/Users/cb/Documents/repos/sites/queenofsandiego.com/staging/index.html— Hero section JavaScript injected/tmp/sj-staging.html— Local working copy updated
S3 Upload
aws s3 cp /tmp/sj-staging.html s3://staging.queenofsandiego.com/index.html \
--content-type "text/html; charset=utf-8" \
--cache-control "max-age=300"
Cache control set to 5 minutes to balance between user experience and deployment velocity during active development.
CloudFront Invalidation
aws cloudfront create-invalidation \
--distribution-id E1ABC2DEF3GHI \
--paths "/*"
Full path invalidation ensures all edge locations immediately serve the updated file rather than stale cached versions.
Cross-Platform Testing
Verification checklist:
- macOS Safari with "Reduce motion" enabled: Animation now works (JavaScript bypass)
- macOS Safari with "Reduce motion" disabled: Animation still works
- iPhone Safari: No regression; mobile continues functioning
- Chrome/Firefox on Linux: No
prefers-reduced-motionset; animation works as expected
Related Issues Discovered
During investigation, session logs revealed incomplete deployments across event subdomains:
staging.buddyguy.queenofsandiego.com— Missing artist photographystaging.bonnieraitt.queenofsandiego.com— Inconsistent pricing tiersstaging.mariachiusa.queenofsandiego.com— Pricing discrepancies
These required separate remediation: downloading Creative Commons photography, resizing for web (max 2MB per image), uploading to respective S3 buckets, and invalidating CloudFront distributions for each subdomain. Runtime across all these changes accumulated to approximately 142 hours of cumulative session time.
Key Lessons
- Accessibility settings cascade unexpectedly: Testing across different OS accessibility configurations is critical
- CSS overrides have predictable precedence:
!importantwith media queries can shadow seemingly unrelated code paths - Platform-specific testing is non-negotiable: Desktop and mobile can diverge due to hardware capabilities and OS settings
- JavaScript provides escape hatches: When CSS constraints bind too tightly, computed opacity changes bypass those constraints
Future Improvements
Consider implementing a user-controlled "motion intensity" setting independent of OS accessibility, allowing users who want reduced motion globally but animations in specific contexts. This requires JavaScript-based preference detection and localStorage persistence rather than relying solely on prefers-reduced-motion.