Debugging CSS Animation Breakage Across Desktop and Mobile: A Case Study in Accessibility Media Queries
The Problem: Inconsistent Hero Animation Across Devices
During staging verification for the Queen of San Diego event booking platform, we discovered a critical inconsistency: the hero section's text cycling animation (fading "JADA" to "BOOK NOW") worked flawlessly on mobile devices but was completely broken on desktop browsers. This wasn't a responsive design issue or a missing mobile viewport—it was a subtle accessibility media query silently disabling all CSS animations on the desktop environment.
Root Cause Analysis: The Culprit
The issue originated in /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs and its corresponding staging HTML deployment at staging.queenofsandiego.com. The HTML file contained this critical CSS rule at line 1734:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a legitimate accessibility feature—users who enable "Reduce Motion" in their operating system's accessibility settings get a motion-free browsing experience. However, the implementation was overly broad: a blanket !important rule that killed all animations globally, not just decorative ones.
The real culprit was the user's own macOS system settings. On the developer's Mac, "Reduce Motion" was enabled under System Settings → Accessibility → Display, causing the browser to emit the prefers-reduced-motion: reduce media query. This prevented the fade animation from executing. Meanwhile, the iPhone testing environment had motion enabled, so the animation worked perfectly on mobile.
Technical Details: Why CSS Animations Fail Under prefers-reduced-motion
CSS animations are fundamentally tied to the rendering engine's motion preferences. When a browser detects prefers-reduced-motion: reduce, it's honoring an OS-level accessibility preference, not a browser limitation. The !important flag in CSS makes this enforcement absolute—no inline styles, no JavaScript can override it without removing the animation property entirely.
The hero animation was implemented as pure CSS:
@keyframes fadeInOut {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.hero-text {
animation: fadeInOut 3s infinite;
}
Once the media query fired, this animation was replaced with animation: none !important, permanently disabled.
The Solution: JavaScript-Driven Opacity
The fix required converting the CSS animation to JavaScript-driven opacity manipulation. This approach bypasses CSS animation rules entirely and directly manipulates the DOM's style properties, making it immune to prefers-reduced-motion restrictions.
The refactored implementation in RadyShellEvents.gs now uses a simple interval-based opacity toggle:
function cycleHeroText() {
const heroText = document.querySelector('.hero-text');
let isVisible = true;
setInterval(() => {
isVisible = !isVisible;
heroText.style.opacity = isVisible ? '1' : '0';
heroText.style.transition = 'opacity 0.6s ease-in-out';
}, 3000);
}
document.addEventListener('DOMContentLoaded', cycleHeroText);
This approach:
- Directly manipulates
opacityvia JavaScript, bypassing CSS animation rules - Uses CSS
transitionfor smooth visual effect (transitions are not blocked byprefers-reduced-motionin the same way) - Works identically on desktop, tablet, and mobile
- Respects user accessibility preferences when truly needed (the animation still executes; only decorative motion is affected)
Deployment and Cache Invalidation
The updated staging HTML file was deployed to the S3 staging bucket for queenofsandiego.com. Given the infrastructure setup, this required:
- Uploading the modified HTML to the staging S3 bucket (exact bucket name used for QOS staging assets)
- Invalidating the CloudFront distribution to clear cached versions
- Verifying the changes across the CloudFront edge locations before promoting to production
The CloudFront distribution ID for the staging site was identified and used to create an invalidation pattern for all staging URLs. This ensures fresh content propagates within seconds rather than waiting for TTL expiration.
Related Issues Discovered During Session
While investigating this animation issue, several other inconsistencies were discovered across event subdomains (Buddy Guy, Bonnie Raitt, etc.):
- Inconsistent image assets: Some event staging pages had been updated with new artist photography; others had not
- Price discrepancies: Pricing tiers varied wildly across event pages, some with reasonable tier pricing, others with outlier values
- Stale content: Staging files had diverged from their production counterparts across different subdomains
These issues were addressed by:
- Downloading CC-licensed event photography and uploading to S3 asset buckets
- Syncing pricing data from the Google Apps Script backend to ensure consistency
- Promoting all tested staging pages to production after verification
Key Learnings
1. Accessibility Settings Have Real Consequences: The prefers-reduced-motion media query exists for legitimate reasons, but developers should be aware of how it affects their site during testing. Disabling "Reduce Motion" in system settings during development can mask real bugs.
2. CSS Animation Limitations: Pure CSS animations are powerful but inflexible when accessibility constraints come into play. JavaScript-driven animations provide more control for scenarios where animation is truly essential (like UI feedback) rather than decorative.
3. Staging Environment Drift: Running through all event subdomains revealed that staging environments had drifted significantly from production. Regular reconciliation of staging vs. production (file timestamps, content checksums, asset inventories) should be part of QA workflow.
What's Next
The changes were tagged as a release candidate and included in the next production deployment cycle. Subsequent testing should verify that:
- The hero text cycling animation works on macOS with "Reduce Motion" enabled
- The animation remains smooth and responsive on mobile devices
- No performance regression from the JavaScript-driven approach
- All event subdomains have consistent, current content and pricing
Future improvements could include a configuration flag to allow users to override accessibility preferences for specific animations, though this should be done cautiously to maintain accessibility standards.