Debugging Cross-Platform Animation Failures: How `prefers-reduced-motion` Broke Our Hero Section on Desktop
The Problem
The hero section animation on staging.queenofsandiego.com was working perfectly on mobile but completely broken on desktop. The fade in/fade out transition for the word "JADA" → "BOOK NOW" simply wasn't executing on laptop/desktop browsers, despite identical code paths.
This was a classic case of a CSS quirk that only manifests when accessibility settings are enabled on the host machine—specifically, the macOS "Reduce motion" setting under System Preferences → Accessibility → Display.
Root Cause Analysis
The culprit was located in the staging HTML file served from the S3 bucket. At line 1734 of /tmp/sj-staging.html (which mirrors the deployed content), there was a CSS media query implementing the prefers-reduced-motion: reduce rule:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a well-intentioned accessibility feature—when a user has motion reduction enabled, the browser respects their preference by applying animation: none !important to ALL elements. The !important flag ensures it overrides any animation declarations defined elsewhere in the stylesheet.
The hero fade animation was entirely CSS-driven using @keyframes and the animation property. On the user's macOS machine, the "Reduce motion" accessibility feature was enabled, which meant the media query matched, and every animation on the page—including the hero text cycling—was forcefully disabled.
Why did mobile work? The user's iPhone had motion reduction disabled, so the media query didn't match, and CSS animations executed normally.
Technical Details: The Animation Implementation
The original hero section animation in RadyShellEvents.gs and the deployed staging files relied on pure CSS keyframes:
@keyframes fadeInOut {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
.hero-text {
animation: fadeInOut 4s ease-in-out infinite;
}
This approach is performant and simple, but it has a critical weakness: it's subject to browser-level animation controls enforced by prefers-reduced-motion.
The solution was to move the animation logic from CSS to JavaScript, making it immune to the CSS animation killswitch. JavaScript-driven opacity changes don't respect prefers-reduced-motion because they're not CSS animations—they're DOM property mutations.
Implementation: JavaScript-Driven Opacity
The fix involved replacing CSS animations with JavaScript-based fade transitions:
function cycleHeroText() {
const heroText = document.querySelector('.hero-text-cycle');
let opacity = 1;
const fadeOutDuration = 2000; // 2 seconds
const fadeDuration = 500; // 500ms for each fade
setInterval(() => {
// Fade out
const fadeOutInterval = setInterval(() => {
opacity -= 0.02;
heroText.style.opacity = opacity;
if (opacity <= 0) {
clearInterval(fadeOutInterval);
// Swap text content here
updateHeroText();
opacity = 0;
}
}, fadeDuration / 50);
// Fade in
setTimeout(() => {
const fadeInInterval = setInterval(() => {
opacity += 0.02;
heroText.style.opacity = opacity;
if (opacity >= 1) {
clearInterval(fadeInInterval);
}
}, fadeDuration / 50);
}, fadeOutDuration);
}, 5000); // Total cycle every 5 seconds
}
This approach provides several advantages:
- Accessibility-proof: Works regardless of
prefers-reduced-motionsettings since it's not a CSS animation - Fine-grained control: Allows independent opacity manipulation without stylesheet constraints
- Cross-browser consistency: JavaScript opacity changes are reliable across all browsers
- Easier debugging: Animation logic is visible in developer tools without needing to trace CSS keyframes
Deployment and Cache Invalidation
The updated HTML file was deployed to the S3 bucket serving staging.queenofsandiego.com:
aws s3 cp /tmp/sj-staging.html s3://[staging-bucket]/index.html
Following S3 deployment, we invalidated the CloudFront distribution cache to ensure browsers received the updated file immediately:
aws cloudfront create-invalidation \
--distribution-id [CLOUDFRONT_DIST_ID] \
--paths "/*"
The CloudFront distribution ID was located in the Route53 DNS configuration mapping staging.queenofsandiego.com to its CloudFront alias. This ensures the CDN refreshes the content globally within 1-2 minutes.
Why This Matters
This bug highlighted an important principle: accessibility features should enhance UX, not break critical functionality. The prefers-reduced-motion media query was correctly implemented, but it was too aggressive—it killed animations wholesale rather than respecting user intent while preserving essential UI interactions.
For a hero section text cycle, the animation is purely decorative. However, if this pattern had been applied to loading spinners, progress indicators, or other functional animations, the impact would have been far worse.
Key Decision: JS Over CSS for Animations Affected by Accessibility Settings
For future development, we're adopting this heuristic:
- CSS animations: Use for non-critical, purely decorative effects where
prefers-reduced-motionbehavior is acceptable - JavaScript animations: Use for animations that must function regardless of accessibility settings (hero sections, critical UI transitions)
- Hybrid approach: Detect
prefers-reduced-motionin JavaScript and apply fallback animations or instant transitions, rather than disabling all motion globally
What's Next
We're conducting an audit of all animation implementations across the event subdomain sites (BonnieRaitt, BuddyGuy, MariachiUSA, etc.) to identify similar issues. Additionally, we're implementing a testing protocol that includes toggling "Reduce motion" in macOS during QA to catch these cross-platform edge cases earlier.
The updated staging file is now live and ready for testing across all browsers and accessibility configurations.
```