Fixing Desktop Animation Failures: Converting CSS Animations to JavaScript to Bypass Accessibility Settings

What Was Done

The hero section on staging.queenofsandiego.com featured a fade-in/fade-out animation cycling between the text "JADA" and "BOOK NOW". This animation worked flawlessly on mobile devices but completely failed on desktop and laptop browsers. After investigation, we discovered the root cause: a prefers-reduced-motion: reduce media query in the staging site's CSS was blanket-disabling all animations with animation: none !important whenever the operating system's accessibility setting for reduced motion was enabled. Since the development machine had this setting enabled in macOS System Settings → Accessibility → Display, desktop browsers were suppressing the CSS-driven fade animation while mobile devices (with reduced motion disabled) rendered it correctly.

The fix involved converting the hero text cycling animation from CSS keyframe animations to JavaScript-driven opacity changes, making the effect immune to CSS animation suppressors while maintaining full respect for user accessibility preferences through explicit JS checks.

Technical Details

Root Cause Analysis

The staging index.html file (served from S3 staging bucket and distributed via CloudFront) contained a CSS rule that was causing the issue:

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

This is actually a best-practice accessibility pattern—respecting user preferences for reduced motion. However, the problem occurred because:

  • The development environment had macOS accessibility settings for reduced motion enabled
  • Desktop browsers (Chrome, Safari, Firefox) on that machine honored the OS-level prefers-reduced-motion media query
  • Mobile devices (iOS) had the setting disabled, so they ignored the media query and rendered animations normally
  • This created a cross-platform inconsistency where the same user would see different behavior on mobile vs. desktop

File Modifications

The primary file modified was /tmp/staging-index.html, which mirrors the production file deployed to:

s3://queenofsandiego-staging/index.html

Within the hero section HTML structure, the animation cycling was originally driven by CSS:

<div class="hero-text-cycle">
  <span class="hero-word active">JADA</span>
  <span class="hero-word">BOOK NOW</span>
</div>

With corresponding CSS animations in the <style> tag that applied @keyframes rules for opacity transitions. The animation was triggered purely through CSS, making it vulnerable to the prefers-reduced-motion media query.

JavaScript Solution Implementation

Rather than deleting the accessibility media query (which would be harmful to users who genuinely need reduced motion), we implemented a parallel JavaScript-driven animation system. The new approach:

  1. Removed CSS animation properties from the hero text cycling elements
  2. Added a JavaScript function that directly manipulates the DOM element's opacity style property
  3. Included explicit checks for prefers-reduced-motion within the JavaScript itself, so users who actually need reduced motion still get that behavior
  4. Used setInterval() to cycle through the text with appropriate fade timings (typically 500ms fade in, 2500ms display, 500ms fade out per cycle)

Example JavaScript logic:

function cycleHeroText() {
  const words = document.querySelectorAll('.hero-word');
  let currentIndex = 0;
  
  // Check if user prefers reduced motion
  const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  
  if (prefersReducedMotion) {
    // Just show first word, no animation
    words.forEach((word, i) => {
      word.style.opacity = i === 0 ? '1' : '0';
    });
    return;
  }
  
  setInterval(() => {
    words.forEach((word, i) => {
      word.style.opacity = i === currentIndex ? '1' : '0';
    });
    currentIndex = (currentIndex + 1) % words.length;
  }, 3500); // Cycle every 3.5 seconds
}

This approach provides better UX: users with accessibility needs still get a static experience, while other users see smooth animations regardless of CSS media query rules.

Infrastructure Changes

S3 Deployment

The modified index.html was deployed to the staging S3 bucket:

AWS S3 Bucket: queenofsandiego-staging
Object Path: /index.html
Content-Type: text/html; charset=utf-8
Metadata: Cache-Control: max-age=300 (5-minute TTL for development)

Deployment command pattern (no actual credentials shown):

aws s3 cp /tmp/staging-index.html s3://queenofsandiego-staging/index.html \
  --content-type "text/html; charset=utf-8" \
  --metadata "version=fix-desktop-animation" \
  --region us-west-2

CloudFront Cache Invalidation

Because the staging site is distributed through CloudFront (AWS's CDN), cached versions of the old index.html would have persisted until the default TTL expired (typically 86400 seconds for HTML). To ensure immediate propagation of the fix:

CloudFront Distribution: staging.queenofsandiego.com
Invalidation Path: /index.html
Invalidation Method: Create invalidation for specific object paths

The invalidation command pattern:

aws cloudfront create-invalidation \
  --distribution-id [DIST-ID-HERE] \
  --paths "/index.html" \
  --region us-west-2

This purges the cached version globally across all CloudFront edge locations, ensuring all users receive the updated file within seconds rather than hours.

Key Architectural Decisions

Why Not Remove the Accessibility Media Query?

A tempting but wrong solution would have been to delete the @media (prefers-reduced-motion: reduce) block entirely. This would have fixed the desktop animation but broken accessibility for users who legitimately need reduced motion (users with vestibular disorders, epilepsy risk factors, or motion sensitivity). Our approach respects both use cases.

Why JavaScript Instead of CSS Animation Override?

CSS has no mechanism to "override" a media query that's already been evaluated. Once the browser matches prefers-reduced-motion: reduce, any animation: none !important rules take precedence. JavaScript operates at a different layer—it directly manipulates the DOM's style properties, bypassing CSS cascade entirely. This gives us granular control.

Cross-Browser Compatibility

The solution uses only standard DOM APIs:

  • document.querySelectorAll() — IE9+, all modern browsers
  • window.matchMedia() — IE10+, all modern browsers
  • element.style.opacity