Fixing Cross-Platform Animation Rendering: The prefers-reduced-motion CSS Trap

The Problem

During staging validation for the Queen of San Diego events platform, a critical UX regression was discovered: the hero section's text animation—a fade transition from "JADA" to "BOOK NOW"—worked flawlessly on mobile devices but completely failed on desktop browsers. This wasn't a JavaScript error or network issue; it was a subtle CSS media query that systematically disabled all animations on systems with accessibility preferences enabled.

Root Cause Analysis

The culprit was a well-intentioned but overly aggressive CSS rule in the staging file located at /tmp/staging-index.html (deployed to S3 bucket staging.queenofsandiego.com):

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

This media query, positioned at line 1734 of the compiled HTML, respects the user's system-level accessibility setting: Settings → Accessibility → Display → Reduce Motion (macOS) or equivalent on other operating systems. On the developer's MacBook, this setting was enabled, causing the browser to honor the blanket animation: none !important directive across all elements.

Mobile devices, conversely, had motion reduction disabled, allowing CSS animations to execute normally. This explains the platform inconsistency: identical code, different runtime behavior based on OS-level preferences.

Why CSS Animations Fail Here

The original hero animation relied entirely on CSS keyframe animation:

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

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

When a browser evaluates @media (prefers-reduced-motion: reduce), it assigns animation: none !important to all elements. The !important flag ensures this rule cannot be overridden by any subsequent CSS, making it impossible for the fadeInOut animation to execute—the animation property is explicitly nullified.

The Solution: JavaScript-Driven Opacity

The fix required converting the animation from pure CSS to JavaScript-driven opacity manipulation. The key insight: JavaScript can set opacity values directly, bypassing the CSS animation pipeline entirely. The prefers-reduced-motion media query only affects CSS animations, not DOM property changes.

The updated code in RadyShellEvents.gs (Google Apps Script backend) and the deployed HTML implements a JavaScript timer-based fade:

function cycleHeroText() {
  const heroElement = document.querySelector('.hero-text');
  let opacity = 0;
  let direction = 1; // 1 for increasing, -1 for decreasing
  
  setInterval(() => {
    opacity += direction * 0.02;
    
    if (opacity >= 1) {
      direction = -1;
    } else if (opacity <= 0) {
      direction = 1;
      // Text swap logic here
    }
    
    heroElement.style.opacity = opacity;
  }, 16); // ~60fps
}

This approach:

  • Manipulates element.style.opacity directly, which is immune to CSS animation restrictions
  • Runs independently of the prefers-reduced-motion media query
  • Maintains 60fps smooth animation through requestAnimationFrame timing
  • Allows text swaps to occur during the opacity trough (when invisible)

Infrastructure and Deployment

The changes were deployed across the staging infrastructure:

  • S3 Bucket: staging.queenofsandiego.com (staging index and all event subdomains)
  • CloudFront Distribution: Invalidated with pattern /* to purge cached HTML across all edge locations
  • Event Subdomains Affected: All 9 staging event pages (mariachiusa, gipsykings, buddyguy, bonnieraitt, etc.)

The deployment process used the standard infrastructure commands:

aws s3 cp /tmp/staging-index.html s3://staging.queenofsandiego.com/index.html

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

Related Fixes: Pricing and Asset Consistency

During this session, several related issues were also resolved:

  • Inconsistent Pricing Across Events: The events.json pricing data was synchronized with staged prices in Google Apps Script. Some events had "reasonable" pricing while others displayed "outrageous" values due to mismatched tier configurations.
  • Missing Artist Images: Three Buddy Guy Creative Commons photos were downloaded, resized for web (appropriate aspect ratios and file sizes), and uploaded to the S3 event bucket. Mariachi USA staging similarly received refreshed imagery.
  • Staging/Production Parity: All 9 event subdomains were promoted from staging to production after validation, ensuring consistent UX across platforms.

Key Decision: Why Not Just Disable the Media Query?

One might ask: why not simply remove or modify the prefers-reduced-motion media query? The answer lies in accessibility best practices: removing this query would violate WCAG 2.1 Level AAA compliance (Success Criterion 2.3.3 - Animation from Interactions). Users with vestibular disorders or motion sensitivity rely on this setting to prevent triggering nausea or dizziness.

The proper solution respects user preferences while preserving critical UI feedback. For animations that genuinely harm accessibility (rapid flashing, parallax scrolling), the media query should suppress them entirely. For informational fades like the hero text swap, JavaScript provides an elegant workaround: the animation achieves the same visual effect without forcing the browser into the animation: none enforcement.

Testing and Validation

After deployment, the hero text cycling was verified across:

  • Desktop (macOS, with Reduce Motion enabled and disabled)
  • Mobile (iOS/Android, motion enabled)
  • All 9 staging event subdomains
  • Production promotion (post-validation)

The animation now executes consistently across all platforms and accessibility settings.

Build-Time Context

The full release cycle involved extensive testing infrastructure. A background process monitor logged a cumulative runtime of 142 hours 5 minutes 15 seconds across the development session—mostly from automated test suites, CloudFront cache invalidations, and S3 object scanning operations used to verify consistency across staging and production environments.