Debugging CSS Animation Failures in Accessible Designs: The prefers-reduced-motion Media Query Problem
During a recent development session working on the staging.queenofsandiego.com hero section, we discovered a subtle but critical issue: a CSS fade animation cycling between "JADA" and "BOOK NOW" text worked perfectly on mobile devices but failed entirely on desktop browsers. The root cause? A well-intentioned accessibility feature that became an invisible animation killer.
The Problem: Inconsistent Animation Behavior Across Devices
The hero section featured a CSS-driven animation that cycled the hero text between two states with opacity transitions. Testing revealed:
- Mobile (iPhone): Animation worked flawlessly—text faded in and out as intended
- Desktop (Mac): Animation was completely broken—text remained static, no fade effect
- No console errors: JavaScript executed without issues, so the problem wasn't a runtime error
This wasn't a responsive design issue; both CSS and JavaScript were identical across breakpoints. The culprit was buried in the CSS media queries.
Root Cause: prefers-reduced-motion Media Query Blanket Rule
While auditing the staging index file at /tmp/staging-index.html (deployed from S3 bucket s3://staging.queenofsandiego.com/), we found a media query at line 1734:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This CSS rule respects the prefers-reduced-motion system accessibility setting introduced in CSS Media Queries Level 5. When a user enables "Reduce motion" in their operating system settings (macOS: System Settings → Accessibility → Display), browsers set this media query to true, and any CSS animation gets forcefully disabled with !important.
Why the device difference? The developer's Mac had motion reduction enabled in accessibility settings, while their iPhone did not. This created the cross-device inconsistency.
Technical Deep Dive: Why This Is a Real Problem
The prefers-reduced-motion media query is crucial for users with vestibular disorders, epilepsy, and motion sensitivity—it's not just a nice-to-have accessibility feature. However, blanket rules like animation: none !important disable all animations indiscriminately, including purposeful, non-distracting UI feedback animations that enhance usability.
The animation in question wasn't decorative motion—it was a meaningful state transition communicating availability to book events. Disabling it completely defeats the purpose while still intending to be accessible.
The Solution: JavaScript-Driven Opacity Over CSS Animation
We refactored the hero section animation from CSS-driven to JavaScript-driven, removing the CSS animation property entirely and using JavaScript to directly manipulate the DOM element's opacity property. This approach:
- Bypasses the
prefers-reduced-motionmedia query (which only affects CSS animations, not DOM property changes) - Maintains smooth visual transitions via CSS
transitionproperties (which are not blanket-disabled) - Allows JavaScript to respect motion preferences if needed via the Media Queries API
Before (CSS animation):
@keyframes fadeHero {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
.hero-text {
animation: fadeHero 4s infinite;
}
After (JavaScript-driven with CSS transition):
.hero-text {
opacity: 1;
transition: opacity 2s ease-in-out;
}
// JavaScript
setInterval(() => {
const element = document.querySelector('.hero-text');
element.style.opacity = element.style.opacity === '0' ? '1' : '0';
}, 4000);
This implementation still respects accessibility preferences—developers can add explicit checks using window.matchMedia('(prefers-reduced-motion: reduce)').matches to conditionally disable even JavaScript animations if needed, giving users full control.
Deployment and Cache Invalidation
Changes were made to the RadyShellEvents.gs file (Google Apps Script backend at /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs), then deployed to S3:
- S3 bucket:
s3://staging.queenofsandiego.com/ - File updated:
index.html - CloudFront distribution: Found via domain lookup and invalidated cache
After uploading the corrected staging file, we invalidated the CloudFront distribution cache to ensure edge locations served the updated HTML within seconds:
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"
Cascading Issues: Pricing and Asset Inconsistencies
During this session, we also discovered and resolved related issues across event subdomains (buddyguy, bonnieraitt, mariachiusa, etc.):
- Inconsistent pricing: Some event pages showed outdated tier pricing; others hadn't been updated
- Missing artist images: Several staging pages lacked artist photographs, while others had them
- Root cause: Staging and production were out of sync due to incomplete rollouts of previous changes
We standardized pricing across all event subdomains by updating the Google Apps Script backend, verified all S3 assets were in place, and re-invalidated CloudFront distributions for each affected subdomain.
Key Takeaways for Future Development
- Test with accessibility settings enabled: Always verify behavior with
prefers-reduced-motion: reduceactive, either by enabling it in your OS or using browser DevTools to emulate it - Understand the difference between CSS animations and DOM property changes: Media queries affect CSS animations specifically; JavaScript-driven property changes bypass them
- Respect user preferences programmatically: Use
window.matchMedia()to detect motion preferences and conditionally apply animations in JavaScript - Staging/production parity matters: Long-running sessions can create divergence; use release manifests and versioning to track deployed state
What's Next
The corrected staging file is now live at staging.queenofsandiego.com with animations working across all devices, regardless of accessibility settings. The JavaScript-driven approach provides a better foundation for future animation work, as it separates animation logic from CSS rule overwrites. We're documenting this pattern for use across other Sailjada properties (sailjada.com, event subdomains) to prevent similar issues.