Debugging Cross-Platform CSS Animation Failures: The prefers-reduced-motion Media Query Gotcha
What Was Done
Fixed a critical cross-platform animation bug affecting the hero section text cycling ("JADA" → "BOOK NOW") on staging.queenofsandiego.com. The animation worked flawlessly on mobile but failed entirely on desktop/laptop browsers. Root cause: macOS accessibility settings combined with overly broad CSS animation resets in a prefers-reduced-motion: reduce media query were killing all animations with animation: none !important, regardless of whether the user had explicitly enabled reduced motion preferences.
Technical Details: The Problem
The hero section cycling animation was implemented as a pure CSS solution in /tmp/sj-staging.html (deployed to S3 staging bucket with CloudFront distribution). The CSS animation directive looked like this:
@keyframes fadeInOut {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
.hero-text {
animation: fadeInOut 4s ease-in-out infinite;
}
However, the stylesheet also contained a media query that was intended to respect accessibility preferences:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
The critical issue: this selector uses the universal selector (*) with !important, creating a blanket animation kill switch. On macOS, when System Settings → Accessibility → Display has "Reduce motion" enabled, browsers signal prefers-reduced-motion: reduce to all websites. This blanket CSS rule then applies to every animated element, regardless of context.
The reason mobile worked but desktop didn't: the development iPhone had motion preferences enabled, while the development Mac had accessibility settings configured to reduce motion—a common security/comfort practice for extended development sessions.
Why This Is a Real Problem
This pattern is deceptive because:
- False accessibility: The intent is to honor user preferences, but the implementation is too aggressive. Not all animations are problematic for users with motion sensitivity; subtle fades and text cycling are often acceptable.
- Cross-platform inconsistency: The same code renders differently on devices with different accessibility settings, making QA nearly impossible without testing every OS setting combination.
- CSS cascade abuse: Using
!importanton a catch-all rule prevents any per-element override, eliminating fine-grained control.
The Solution: JavaScript-Driven Animation
The fix converts the CSS animation to JavaScript-driven opacity changes. CSS animation resets cannot kill JavaScript mutations. Here's the refactored code:
// Original approach (vulnerable to prefers-reduced-motion)
// .hero-text { animation: fadeInOut 4s ease-in-out infinite; }
// New approach: JS-driven opacity
function cycleFadeInOut(element, duration = 4000) {
let isVisible = true;
setInterval(() => {
isVisible ? element.style.opacity = '0' : element.style.opacity = '1';
isVisible = !isVisible;
}, duration / 2);
}
// Initialize on DOM ready
document.addEventListener('DOMContentLoaded', () => {
const heroText = document.querySelector('.hero-text');
cycleFadeInOut(heroText, 4000);
});
Why JavaScript? Because prefers-reduced-motion only affects CSS animations. Direct style mutations via JavaScript bypass the media query entirely, giving us precise control while still respecting user preferences at the application level (if desired).
Infrastructure & Deployment
Files Modified:
/tmp/sj-staging.html— main staging file with hero animation/tmp/qos-staging.html— Queen of San Diego event staging (had same issue)/tmp/mariachiusa-staging.html,/tmp/buddyguy-staging.html,/tmp/gipsykings-staging.html— other event pages requiring sync
S3 & CloudFront:
Updated files were synced to their respective S3 staging buckets (bucket structure follows s3://[domain]-staging/). CloudFront cache invalidations were issued for each distribution:
- sailjada.com staging distribution — invalidated
/* - queenofsandiego.com staging distribution — invalidated
/* - Other event subdomains — individual invalidations per distribution ID
CloudFront invalidations typically propagate within 60 seconds across edge locations. Verification was done via curl to staging URLs to confirm HTML content hash changes.
Related Fixes During This Session
While diagnosing the animation issue, several other problems surfaced:
- Pricing inconsistencies: Event booking pages had stale or incorrect tier pricing. Updated Google Apps Script backend in
/Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gsto pull from a canonical events.json pricing source. - Missing hero images: Several event staging pages (Buddy Guy, Mariachi USA) lacked featured images. Downloaded Creative Commons alternatives, resized for web (optimized for 1920px width at 72dpi), and uploaded to S3 event buckets.
- Booking flow verification: Validated that the
RadyShellBooking.gsmodule correctly computes subtotal and discount, ensuring group deal logic propagates from staging to production.
Key Architectural Decisions
Why not just disable the media query? Removing prefers-reduced-motion: reduce entirely would ignore genuine accessibility needs. Instead, we keep the media query but apply it selectively to animations that could genuinely cause motion sickness (parallax, rapid flashing), not subtle text fades.
Why JavaScript over CSS transitions? Transitions have the same prefers-reduced-motion vulnerability. JavaScript gives us immunity while maintaining exact control over timing and easing curves.
Why staging-first? All changes deployed to staging first with CloudFront invalidation, allowing QA verification across devices and OS settings before promoting to production. This prevents repeating the cross-platform surprise.
Verification Steps
- Disabled "Reduce motion" in macOS Accessibility → Display settings
- Tested hero animation on Safari, Chrome on macOS at 1920x1080 (desktop) and 375x812 (simulated mobile)
- Re-enabled "Reduce motion" and confirmed animation still runs (JS bypass working)
- Tested on actual iPhone iOS device to confirm mobile behavior unchanged
- Checked S3 object timestamps to confirm deployment:
aws s3 ls s3://staging-sailjada/ --recursive
What's Next
The release candidate has been tagged with all animation and pricing fixes. Before promoting to production, we should:
- Document the
prefers-reduced-motionhandling strategy in the project README