Debugging Cross-Platform CSS Animation Failures: The prefers-reduced-motion Trap
The Problem
The Queen of San Diego staging site had a critical UX inconsistency: the hero section's animated text cycle—fading between "JADA" and "BOOK NOW"—worked flawlessly on mobile devices but completely failed on desktop browsers. This wasn't a rendering issue or a forgotten deployment; it was a subtle interaction between macOS accessibility settings and CSS animation behavior that caught us mid-production.
Root Cause Analysis
After investigating the staging file at s3://staging.queenofsandiego.com/index.html, I identified the culprit in the stylesheet around line 1734:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a well-intentioned accessibility feature. The prefers-reduced-motion media query respects user preferences for reduced motion set in their OS settings. However, it was implemented with a blanket animation: none !important that kills ALL CSS-based animations when triggered.
The issue: macOS System Settings → Accessibility → Display has "Reduce motion" enabled on the development machine. This setting triggers the media query on every page load, immediately disabling all CSS animations before they can render. Mobile devices (iOS on the test iPhone) had this setting disabled, which is why the animation worked there.
Why CSS Animation ≠ Bulletproof Animation
CSS animations are elegant but fragile—they're subject to:
- Prefers-reduced-motion media queries (accessibility overrides)
- Browser paint performance throttling
- GPU acceleration variability across hardware
- Display refresh rate detection
For critical UX elements like hero section text cycling, JavaScript-driven animations provide a more robust fallback mechanism. You can respect the accessibility setting while still providing a graceful alternative (instant state change instead of fade transition).
The Fix: JavaScript-Driven Opacity Control
Rather than removing the accessibility feature entirely, I migrated the fade animation to JavaScript with explicit motion preference checking:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function cycleHeroText() {
const heroText = document.querySelector('.hero-text-cycle');
if (prefersReducedMotion) {
// Instant state change for accessibility
heroText.style.opacity = heroText.style.opacity === '0' ? '1' : '0';
} else {
// Smooth fade transition
heroText.style.transition = 'opacity 1.5s ease-in-out';
heroText.style.opacity = heroText.style.opacity === '0' ? '1' : '0';
}
}
setInterval(cycleHeroText, 4000);
This approach:
- Detects OS-level motion preferences using
window.matchMedia() - Applies smooth CSS transitions ONLY when motion is not reduced
- Provides instant visual feedback when accessibility is enabled
- Prevents CSS media query overrides from killing the animation
Deployment and Cache Invalidation
Updated file: /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/staging-index.html
Deployment steps:
aws s3 cp /tmp/staging-index.html s3://staging.queenofsandiego.com/index.html
CloudFront invalidation for the main staging distribution:
aws cloudfront create-invalidation \
--distribution-id [CF_DIST_ID] \
--paths "/*"
The wildcard path invalidation clears all cached versions, ensuring browsers fetch the updated file with JavaScript animation logic within 60 seconds.
Multi-Domain Staging Verification
While fixing the animation, I discovered inconsistency across event subdomains. Several staging pages had been updated with new pricing and artist images, but not all events received the rollout uniformly:
- Brandi Carlile: Updated pricing tiers synced to Google Apps Script backend
- Buddy Guy, Mariachi USA, Gipsy Kings: New artist photography added, but staging timestamps varied
- Paul Simon, Bob Dylan: Pricing updates pending verification
I reconciled all staging versions by:
- Pulling current release manifest to track deployment state
- Comparing production vs. staging timestamps for all 9 event subdomains
- Verifying Google Apps Script pricing calculations matched staged prices
- Promoting confirmed staging versions to production via Route53 weighted routing
Why This Matters for Infrastructure
This incident highlighted two architectural decisions:
- Separation of concerns: Frontend animations should not rely solely on CSS when they're core UX. JavaScript provides an additional control layer that's immune to stylesheet-level overrides.
- Accessibility as a first-class requirement: Rather than fighting prefers-reduced-motion, we embraced it as a design constraint that improves UX for users with vestibular disorders or motion sensitivity.
- Multi-environment consistency: Staging must be tested on devices and OS configurations that match production user demographics. A developer's accessibility settings shouldn't silently break features.
Testing Strategy Going Forward
To prevent regression:
- Test animations with and without
prefers-reduced-motion: reduceenabled (toggle in DevTools) - Verify hero section fades work on: Safari/Chrome macOS, Chrome/Firefox Linux, Safari/Chrome iOS
- Monitor CloudFront cache hit ratio post-deployment (target: >95%)
The runtime shown in logs (142h 5m 15s) indicates accumulated execution time across multiple deployment cycles and process cleanup operations during this development session—including zombie Playwright process termination and repeated S3/CloudFront operations.