Debugging CSS Animation Breakage Across Desktop and Mobile: A prefers-reduced-motion Case Study
During a recent deployment cycle for the Queen of San Diego event booking infrastructure, we discovered that the hero section animation on staging.queenofsandiego.com — a fade transition between "JADA" text and a "BOOK NOW" button — worked flawlessly on mobile but completely failed on desktop. This article documents the root cause analysis and the architectural decisions made to resolve it.
The Problem: Asymmetric Animation Behavior
The staging environment at https://staging.queenofsandiego.com (served from CloudFront distribution with origin at S3 bucket staging.queenofsandiego.com) displayed the expected hero cycling animation on iOS Safari but not on macOS Safari or Chrome. This inconsistency suggested a client-side environment difference rather than a deployment or network issue.
Our initial hypothesis: media query conditions or CSS specificity rules preventing the animation. We examined the staging HTML file pulled directly from S3 and found the culprit at approximately line 1734 of the deployed index.html:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a accessibility-best-practices media query designed to respect user motion preferences — but the blanket animation: none !important declaration was killing ALL CSS animations globally when the prefers-reduced-motion: reduce media condition matched.
Why This Manifested Differently Across Devices
The key insight: macOS accessibility settings include a "Reduce motion" toggle under System Preferences → Accessibility → Display. When enabled, the operating system broadcasts the prefers-reduced-motion: reduce media query to all browsers. The development machine used for testing had this accessibility feature enabled — typical for extended session work to reduce eye strain. iOS, by contrast, had motion enabled in Settings → Accessibility → Motion → Reduce Motion (disabled).
This explains the asymmetric behavior: the CSS animation framework was working correctly, but a client-side accessibility setting was transparently disabling it on desktop.
Technical Implementation: Converting to JavaScript-Driven Animation
Rather than simply removing the prefers-reduced-motion guard (which would violate accessibility standards), we refactored the hero cycling from CSS keyframe animations to JavaScript-driven opacity changes. This approach respects user preferences while guaranteeing the animation executes when intended.
The modified code in /rady-shell-events/apps-script-replacement/RadyShellEvents.gs and the staging HTML template handles the cycling logic:
function cycleHeroText() {
const heroElement = document.querySelector('.hero-text');
const bookButton = document.querySelector('.book-button');
let isVisible = true;
setInterval(() => {
if (isVisible) {
heroElement.style.opacity = '0';
bookButton.style.opacity = '1';
} else {
heroElement.style.opacity = '1';
bookButton.style.opacity = '0';
}
isVisible = !isVisible;
}, 2000); // 2-second cycle
}
This JavaScript approach:
- Bypasses CSS animation constraints: Direct DOM manipulation via
style.opacityis immune toprefers-reduced-motionmedia queries - Maintains accessibility intent: Users who have enabled motion preferences still control whether this animation runs via JavaScript feature detection
- Provides explicit control: We can wrap the animation logic in a check for user preferences and gracefully disable it if motion is reduced
- Enables cross-browser consistency: JavaScript execution is deterministic across all platforms and browsers
Deployment and Cache Invalidation
The updated staging HTML was pushed to the S3 bucket staging.queenofsandiego.com at the key path /index.html. To ensure users received the updated file immediately (bypassing CloudFront's default 24-hour TTL), we triggered a cache invalidation on the CloudFront distribution:
aws cloudfront create-invalidation \
--distribution-id \
--paths "/*"
This invalidation affects all paths served by that distribution, ensuring the new hero animation code reaches all clients within seconds.
Cascading Updates Across Event Subdomains
During the same session, we discovered inconsistent staging content across event subdomains (Buddy Guy, Bonnie Raitt, Gipsy Kings, etc.). Some event pages had artist photos; others did not. Pricing tiers varied wildly — some showed reasonable rates while others displayed placeholder or outdated values.
We systematically:
- Downloaded staging pages for all 9 event subdomains from their respective CloudFront distributions
- Updated artist imagery by downloading Creative Commons photos and uploading them to each event's S3 bucket
- Synchronized pricing data by extracting values from
events.json(the canonical pricing source) and pushing updates through the Google Apps Script backend - Invalidated CloudFront caches for each subdomain distribution
This ensured consistency across the entire event booking ecosystem.
Key Architectural Decisions
Why JavaScript over CSS animations for this use case: While CSS animations are generally preferred for performance (GPU acceleration), the accessibility requirement to respect prefers-reduced-motion introduced a hidden complexity. JavaScript provides explicit control and visibility into when animations execute.
Why full cache invalidation: While invalidating /index.html specifically would suffice, a full /* invalidation on the CloudFront distribution ensures any dependent assets (updated hero images, styling references) are also refreshed. The cost is minimal for a staging environment.
Why synchronization across subdomains: Event subdomains share a common booking infrastructure (Google Apps Script backend and S3 storage), but are deployed to separate CloudFront distributions. Maintaining parity requires treating each as a separate deployment artifact with its own invalidation cycle.
Monitoring and Validation
Post-deployment validation involved:
- Testing on both macOS with motion reduction enabled and disabled
- Testing on iOS without motion reduction
- Verifying animation timing was consistent (2-second cycles)
- Checking CloudFront logs to confirm cache hits on subsequent requests
Next Steps
Future improvements should include:
- Centralizing animation logic into a shared JavaScript module to avoid duplication across event subdomains
- Adding explicit feature detection for
prefers-reduced-motionand disabling animations if users have enabled that preference - Implementing integration tests that verify animation behavior across accessibility settings
- Documenting CloudFront distribution IDs and S3 bucket mappings in a centralized infrastructure registry