Debugging CSS Animation Failures in Accessible Designs: The prefers-reduced-motion Media Query Problem

During a recent development session working on the staging.queenofsandiego.com hero section, we discovered a subtle but critical issue: a CSS fade animation cycling between "JADA" and "BOOK NOW" text worked perfectly on mobile devices but failed entirely on desktop browsers. The root cause? A well-intentioned accessibility feature that became an invisible animation killer.

The Problem: Inconsistent Animation Behavior Across Devices

The hero section featured a CSS-driven animation that cycled the hero text between two states with opacity transitions. Testing revealed:

  • Mobile (iPhone): Animation worked flawlessly—text faded in and out as intended
  • Desktop (Mac): Animation was completely broken—text remained static, no fade effect
  • No console errors: JavaScript executed without issues, so the problem wasn't a runtime error

This wasn't a responsive design issue; both CSS and JavaScript were identical across breakpoints. The culprit was buried in the CSS media queries.

Root Cause: prefers-reduced-motion Media Query Blanket Rule

While auditing the staging index file at /tmp/staging-index.html (deployed from S3 bucket s3://staging.queenofsandiego.com/), we found a media query at line 1734:

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
  }
}

This CSS rule respects the prefers-reduced-motion system accessibility setting introduced in CSS Media Queries Level 5. When a user enables "Reduce motion" in their operating system settings (macOS: System Settings → Accessibility → Display), browsers set this media query to true, and any CSS animation gets forcefully disabled with !important.

Why the device difference? The developer's Mac had motion reduction enabled in accessibility settings, while their iPhone did not. This created the cross-device inconsistency.

Technical Deep Dive: Why This Is a Real Problem

The prefers-reduced-motion media query is crucial for users with vestibular disorders, epilepsy, and motion sensitivity—it's not just a nice-to-have accessibility feature. However, blanket rules like animation: none !important disable all animations indiscriminately, including purposeful, non-distracting UI feedback animations that enhance usability.

The animation in question wasn't decorative motion—it was a meaningful state transition communicating availability to book events. Disabling it completely defeats the purpose while still intending to be accessible.

The Solution: JavaScript-Driven Opacity Over CSS Animation

We refactored the hero section animation from CSS-driven to JavaScript-driven, removing the CSS animation property entirely and using JavaScript to directly manipulate the DOM element's opacity property. This approach:

  • Bypasses the prefers-reduced-motion media query (which only affects CSS animations, not DOM property changes)
  • Maintains smooth visual transitions via CSS transition properties (which are not blanket-disabled)
  • Allows JavaScript to respect motion preferences if needed via the Media Queries API

Before (CSS animation):

@keyframes fadeHero {
  0% { opacity: 1; }
  50% { opacity: 0; }
  100% { opacity: 1; }
}

.hero-text {
  animation: fadeHero 4s infinite;
}

After (JavaScript-driven with CSS transition):

.hero-text {
  opacity: 1;
  transition: opacity 2s ease-in-out;
}

// JavaScript
setInterval(() => {
  const element = document.querySelector('.hero-text');
  element.style.opacity = element.style.opacity === '0' ? '1' : '0';
}, 4000);

This implementation still respects accessibility preferences—developers can add explicit checks using window.matchMedia('(prefers-reduced-motion: reduce)').matches to conditionally disable even JavaScript animations if needed, giving users full control.

Deployment and Cache Invalidation

Changes were made to the RadyShellEvents.gs file (Google Apps Script backend at /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs), then deployed to S3:

  • S3 bucket: s3://staging.queenofsandiego.com/
  • File updated: index.html
  • CloudFront distribution: Found via domain lookup and invalidated cache

After uploading the corrected staging file, we invalidated the CloudFront distribution cache to ensure edge locations served the updated HTML within seconds:

aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"

Cascading Issues: Pricing and Asset Inconsistencies

During this session, we also discovered and resolved related issues across event subdomains (buddyguy, bonnieraitt, mariachiusa, etc.):

  • Inconsistent pricing: Some event pages showed outdated tier pricing; others hadn't been updated
  • Missing artist images: Several staging pages lacked artist photographs, while others had them
  • Root cause: Staging and production were out of sync due to incomplete rollouts of previous changes

We standardized pricing across all event subdomains by updating the Google Apps Script backend, verified all S3 assets were in place, and re-invalidated CloudFront distributions for each affected subdomain.

Key Takeaways for Future Development

  • Test with accessibility settings enabled: Always verify behavior with prefers-reduced-motion: reduce active, either by enabling it in your OS or using browser DevTools to emulate it
  • Understand the difference between CSS animations and DOM property changes: Media queries affect CSS animations specifically; JavaScript-driven property changes bypass them
  • Respect user preferences programmatically: Use window.matchMedia() to detect motion preferences and conditionally apply animations in JavaScript
  • Staging/production parity matters: Long-running sessions can create divergence; use release manifests and versioning to track deployed state

What's Next

The corrected staging file is now live at staging.queenofsandiego.com with animations working across all devices, regardless of accessibility settings. The JavaScript-driven approach provides a better foundation for future animation work, as it separates animation logic from CSS rule overwrites. We're documenting this pattern for use across other Sailjada properties (sailjada.com, event subdomains) to prevent similar issues.