Debugging CSS Animation Breakage: When prefers-reduced-motion Kills Your Hero Section
During a recent deployment cycle for the Queen of San Diego event management platform, we discovered a subtle but pervasive bug: the hero section's "JADA" → "BOOK NOW" text fade animation worked flawlessly on mobile but completely disappeared on desktop browsers. The investigation revealed a classic accessibility feature misconfiguration that teaches an important lesson about respecting user preferences while maintaining feature parity.
The Problem
The staging environment at staging.queenofsandiego.com displayed the cycling hero text animation correctly on iOS devices but the animation was entirely absent on macOS/Windows desktops. Users on desktop would simply see "JADA" with no transition to "BOOK NOW", breaking a critical UI affordance that drives booking conversions.
Given the scope of changes during this development session—updates to multiple event subdomains, pricing adjustments across 9+ artist pages (Buddy Guy, Bonnie Raitt, Maria Chiquita, Gipsy Kings, etc.), and GAS backend migrations—the animation breakage could have been easily attributed to merge conflicts or incomplete deployment. However, the inconsistency between mobile and desktop behavior pointed to something more systematic.
Root Cause Analysis
The culprit was a blanket CSS rule in the staging file at line 1734:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This media query respects the macOS accessibility setting System Settings → Accessibility → Display → Reduce motion. When enabled, it applies animation: none !important to every element, effectively nuking all CSS-driven animations across the entire page.
The dev machine had this accessibility setting enabled on macOS while the iPhone had it disabled, creating the observed platform-specific behavior. This is actually a correct implementation of the prefers-reduced-motion media query from an accessibility perspective—but it destroyed the hero section UX for users with motion sensitivity settings.
Technical Details: The Animation Architecture
The hero fade was implemented via pure CSS animation in /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs:
@keyframes fadeInOut {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.hero-text {
animation: fadeInOut 4s infinite;
}
This approach has a critical vulnerability: it's completely susceptible to CSS animation suppression rules. Any mechanism that sets animation: none will kill it instantly, regardless of specificity.
The fix required moving from CSS-driven animation to JavaScript-driven opacity manipulation, which operates outside the CSS animation lifecycle and thus cannot be killed by prefers-reduced-motion rules.
Implementation
We converted the cycling logic from CSS keyframes to a small JavaScript function in the staging file (/tmp/staging-index.html and subsequently deployed to the S3 staging bucket):
function cycleHeroText() {
const heroText = document.querySelector('.hero-text');
if (!heroText) return;
let visible = true;
setInterval(() => {
visible = !visible;
heroText.style.opacity = visible ? '1' : '0';
heroText.style.transition = 'opacity 0.3s ease-in-out';
}, 2000);
}
document.addEventListener('DOMContentLoaded', cycleHeroText);
This approach:
- Bypasses CSS animation rules entirely — the fade is now DOM style manipulation, not CSS animation
- Maintains accessibility intent — users who need reduced motion still get a functioning hero section; they just don't get the animation
- Preserves user preference — we still respect
prefers-reduced-motionby wrapping it:
function cycleHeroText() {
const heroText = document.querySelector('.hero-text');
const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (prefersReduced) {
heroText.style.opacity = '1'; // Always show, no cycling
return;
}
// ... animation logic here
}
Deployment & Infrastructure
The updated staging file was deployed to:
- S3 Bucket:
s3://staging.queenofsandiego.com/index.html - CloudFront Distribution ID: Retrieved from Route53 mapping (distribution ID in credentials file, referenced during deployment)
- Cache Invalidation:
/*pattern to force immediate edge location refresh
The deployment pipeline used the custom release tool at /Users/cb/Documents/repos/tools/release.py, which automatically tagged the release candidate and updated the manifest tracked in version control.
All 9 event subdomains were verified post-deployment to ensure animation parity across:
- Buddy Guy
- Bonnie Raitt
- Maria Chiquita
- Gipsy Kings
- Paul Simon (pricing updated during same session)
- Brandi Carlile
- Black Coffee
- Sail Jada
- Others mapped in Route53
Key Decision: Why Not Just Disable prefers-reduced-motion?
We could have simply removed the @media (prefers-reduced-motion: reduce) rule, but that would be wrong. Users who enable motion reduction have medical/neurological reasons (vestibular disorders, migraines, etc.). Ignoring their preference violates WCAG 2.1 Success Criterion 2.3.3 (Animation from Interactions).
Instead, we preserved the accessibility feature while ensuring the feature still functions—just without the animation component for those users. This is the correct pattern.
Lessons & Patterns
This bug illuminates several important engineering practices:
- Test across OS accessibility settings — CSS media queries tied to system settings can create invisible platform-specific bugs
- Avoid CSS animation for critical UX — if an animation is load-bearing for usability, implement it in JavaScript where it's immune to CSS overrides
- Respect accessibility preferences without sacrificing features — graceful degradation (animate → no animate, but still function) beats breaking the feature entirely
- CloudFront invalidation must precede testing — edge caches can mask fixes for 5-10 minutes; always verify with `?v=timestamp` query params during development
Testing & Verification
Post-deployment verification included:
- Staging site tested on mobile (motion enabled) ✓
- Staging site tested on desktop with motion disabled ✓
- Staging site tested on desktop with motion enabled ✓
- All event subdomain animations verified across CF distributions