Debugging CSS Animation Breakage on Desktop: When Accessibility Settings Silently Kill Your Hero Section
During a development session working on the Queen of San Diego events platform, we discovered a critical issue: the hero section's fade animation between "JADA" and "BOOK NOW" worked flawlessly on mobile but was completely broken on desktop browsers. This post details the root cause, the fix, and the broader lesson about how accessibility settings can silently disable CSS animations across an entire application.
The Problem: Animation Works on Mobile, Dead on Desktop
The staging site at staging.queenofsandiego.com had a CSS-based animation cycling text in the hero section. Testing revealed:
- ✅ Mobile (iPhone): Smooth fade transitions between hero text variants
- ❌ Desktop/Laptop: Text appeared static; no animation
- ❌ Same browser engine; same file served from S3
The immediate assumption—a media query breakpoint or device-specific CSS—turned out to be a red herring. The actual culprit was far more insidious.
Root Cause: The Accessibility Hammer
In the staging file at /tmp/staging-index.html (mirrored in production S3 deployment), a critical CSS rule existed around line 1734:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a well-intentioned accessibility feature—respecting the OS-level "Reduce Motion" setting for users with vestibular disorders or motion sensitivity. However, the implementation was a blunt instrument: animation: none !important kills all CSS animations globally when the user has enabled "Reduce motion" in their system settings.
On macOS, this setting lives in: System Settings → Accessibility → Display → Reduce motion. The developer's laptop had this enabled, but the test iPhone did not—explaining the asymmetry.
Technical Deep Dive: CSS Animation vs. JavaScript-Driven Opacity
The original implementation used pure CSS keyframe animations:
@keyframes fadeInOut {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
.hero-text {
animation: fadeInOut 2s ease-in-out infinite;
}
While elegant and performant, this approach has a critical vulnerability: it respects the prefers-reduced-motion media query, which is correct behavior from an accessibility standpoint, but creates a poor UX when the animation is core to the feature (as opposed to a decorative flourish).
The solution: convert to JavaScript-driven opacity changes, which bypass CSS animation suspension entirely. JavaScript reads the user's motion preference but makes an intentional decision about whether to animate:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function cycleHeroText() {
const heroElement = document.querySelector('.hero-text');
if (prefersReducedMotion) {
// Respect accessibility: no animation, just show text
heroElement.style.opacity = '1';
} else {
// Apply JS-driven fade cycling
let opacity = 0;
const fadeInterval = setInterval(() => {
opacity = Math.sin(Date.now() / 1000) * 0.5 + 0.5;
heroElement.style.opacity = opacity;
}, 30); // ~33fps
}
}
cycleHeroText();
This approach:
- ✅ Still respects accessibility preferences (no jittery motion for sensitive users)
- ✅ Allows intentional animations in functional UI elements
- ✅ Immune to the global
animation: none !importantoverride - ✅ More control over timing and easing curves
Infrastructure & Deployment Changes
The file was deployed across multiple endpoints:
S3 Bucket: s3://staging.queenofsandiego.com/
File: index.html
CloudFront Distribution: (invalidated post-deployment)
The deployment workflow:
- Updated
/tmp/staging-index.htmlwith JS-driven animation logic - Synced to S3:
aws s3 cp staging-index.html s3://staging.queenofsandiego.com/index.html - Invalidated CloudFront cache to force edge node refresh
- Verified propagation across all subdomain CloudFront distributions (bonnieraitt, buddyguy, etc.)
Each artist event subdomain (e.g., staging.bonnieraitt.queenofsandiego.com) has its own CloudFront distribution, so invalidation had to account for all 6+ active event pages. The Route53 alias records all point to their respective CF distribution endpoints, creating a fan-out architecture where a single S3 bucket serves content through multiple distribution IDs.
Related Issues Discovered During Session
While investigating hero animations, secondary inconsistencies surfaced across event staging pages:
- Inconsistent artist imagery: Some event pages displayed hero photos, others showed placeholders. Root cause: image paths in
RadyShellEvents.gsGoogle Apps Script backend were not synchronized across all events. - Pricing discrepancies: Tier prices varied wildly between events (some reasonable, some clearly outdated or test values). Resolution required pushing updated pricing from
events.jsonto the GAS backend.
These were addressed by:
- Updating
/Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gswith normalized image paths and pricing - Tagging a release candidate to capture these fixes for coordinated rollout
- Running the event-list validation command to verify consistency across all subdomains
Key Decision: Embrace Progressively Enhanced Animations
Rather than fighting the accessibility framework, the fix embraces a progressive enhancement model:
- Baseline: Users with reduced-motion preferences see static, instant UI with no animations—fully functional
- Enhanced: Users without motion preferences see smooth, engaging JS-driven fades
- Fallback: If JavaScript fails, CSS-based animations still work for non-reduced-motion users
This pattern respects both user accessibility needs and design intent, rather than treating them as opposing forces.
Lessons for Multi-Tenant Event Platforms
The Queen of San Diego infrastructure distributes individual event pages across separate CloudFront distributions keyed to artist slugs. When fixing hero animations or other global layout issues:
- Always test across multiple subdomains (not just the primary)
- Remember that CloudFront cache invalidation takes ~60 seconds to propagate globally
- Verify backend consistency (GAS pricing, image paths) across all tenant instances before deployment
- Use the manifest-based versioning system to tag coordinated changes
The 142-hour runtime visible in