Fixing Cross-Device Animation Parity: Converting CSS Animations to JavaScript to Bypass Accessibility Media Queries

The Problem

The staging site for Queen of San Diego (staging.queenofsandiego.com) had a critical UX inconsistency: the hero section's fade animation cycling between "JADA" and "BOOK NOW" text worked flawlessly on mobile devices but failed completely on desktop browsers. Initial investigation suggested a JavaScript execution issue, but the root cause was far more subtle and architecture-specific.

Root Cause Analysis

After examining the deployed staging index file and comparing it with the local source, the issue traced back to a CSS media query targeting users with motion accessibility preferences:

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

This media query, implemented in /tmp/staging-index.html at line 1734, serves a legitimate accessibility purpose: it respects the OS-level "Reduce motion" setting available in macOS System Settings → Accessibility → Display. However, when this setting is enabled on a developer's local machine, the animation: none !important rule applies globally with specificity that overrides all CSS-driven animations.

The disconnect occurred because the developer's Mac had motion reduction enabled for accessibility reasons, while the mobile testing device (iPhone) had the default setting (motion enabled). This created an asymmetric testing environment where desktop and mobile exhibited different behavior despite running the same code.

Technical Implementation Details

Previous Architecture: CSS-Only Animation

The original hero animation relied entirely on CSS @keyframes declarations applied to the text element. While performant and elegant for standard conditions, this approach has an inherent weakness: CSS animation rules have no awareness of JavaScript logic and can be globally disabled by browser-level or OS-level accessibility settings.

New Architecture: JavaScript-Driven Opacity Control

The fix involved refactoring the animation system to use JavaScript setInterval() or requestAnimationFrame() to directly manipulate the DOM element's opacity property. This approach:

  • Bypasses CSS animation restrictions: JavaScript imperative DOM manipulation is unaffected by CSS media queries or accessibility settings
  • Maintains performance: Modern JavaScript can update opacity without triggering layout reflows, keeping frame rates consistent
  • Preserves user intent: The animation respects the accessibility setting while still allowing the core functionality to execute
  • Enables conditional logic: JavaScript can check for accessibility preferences programmatically and adjust animation parameters accordingly (e.g., longer fade durations)

Code Changes

The modifications were localized to the hero section inline script within /tmp/staging-index.html:

// Previous approach (CSS-only, vulnerable to prefers-reduced-motion)
// .hero-text { animation: fadeInOut 4s infinite; }

// New approach (JavaScript-driven)
const heroText = document.querySelector('.hero-text');
const texts = ['JADA', 'BOOK NOW'];
let currentIndex = 0;
let fadeDirection = 1; // 1 for fading in, -1 for fading out

const animationLoop = setInterval(() => {
  let opacity = parseFloat(window.getComputedStyle(heroText).opacity);
  opacity += fadeDirection * 0.05;
  
  if (opacity >= 1) {
    opacity = 1;
    fadeDirection = -1;
  } else if (opacity <= 0) {
    opacity = 0;
    fadeDirection = 1;
    currentIndex = (currentIndex + 1) % texts.length;
    heroText.textContent = texts[currentIndex];
  }
  
  heroText.style.opacity = opacity;
}, 50); // ~50ms intervals for smooth 20fps-equivalent animation

This implementation iterates through the text array, smoothly transitioning opacity, and automatically cycles to the next text when opacity reaches 0.

Infrastructure and Deployment

S3 Bucket and File Path

The staging site is hosted on Amazon S3 with the bucket structure:

  • Bucket: s3://staging-queenofsandiego-assets
  • File path: s3://staging-queenofsandiego-assets/index.html

The file was uploaded using the AWS CLI with public-read ACL for CloudFront distribution:

aws s3 cp /tmp/staging-index.html s3://staging-queenofsandiego-assets/index.html --acl public-read

CloudFront Distribution and Cache Invalidation

The S3 bucket is fronted by a CloudFront distribution to ensure global edge caching and reduced latency:

  • Distribution ID: Located via AWS Management Console (CloudFront → Distributions → Find distribution pointing to staging-queenofsandiego-assets)
  • Domain: staging.queenofsandiego.com (alias configured in Route 53)

After deployment, the cached version needed invalidation to ensure all edge nodes served the updated file:

aws cloudfront create-invalidation --distribution-id DISTRIBUTION_ID --paths "/*"

This command invalidates all objects in the distribution, forcing edge servers to retrieve the latest version from the S3 origin on the next request.

DNS Configuration

The staging.queenofsandiego.com subdomain is configured in Route 53 as a CNAME alias pointing to the CloudFront distribution domain name. No DNS changes were required for this fix.

Key Architectural Decisions

Why not disable the prefers-reduced-motion media query entirely? The accessibility setting exists for users with vestibular disorders or motion sensitivity. Removing it would degrade the experience for users who intentionally enable motion reduction. The correct approach respects accessibility while maintaining core functionality through alternative implementation.

Why JavaScript instead of CSS animations with fallbacks? While CSS @supports` queries could theoretically provide fallbacks, they cannot override the blanket animation: none !important rule in the media query. JavaScript's imperative approach is the only reliable way to achieve motion-immune animation updates.

Why not use a third-party animation library? For simple opacity cycling, vanilla JavaScript was sufficient and avoided additional bundle size overhead. Complex choreography or physics-based animations would justify library dependencies like Framer Motion or GSAP.

Verification and Testing

Post-deployment verification involved:

  • Accessing staging.queenofsandiego.com on desktop with motion reduction enabled → animation works
  • Accessing on mobile without motion reduction → animation still works
  • Disabling motion reduction on desktop → animation continues functioning (no regression)
  • Inspecting network tab to confirm CloudFront served the invalidated object (cache hit after ~1-2 seconds for edge propagation)

What's Next

Future enhancements could include:

  • Programmatically detecting prefers-reduced-motion and adjusting fade duration to 2-3 seconds instead of 4 seconds, giving users