```html

Fixing CSS Animation Breakage on Desktop: The prefers-reduced-motion Trap

What Was Done

We identified and resolved a critical issue where the hero section's text cycling animation (fading "JADA" to "BOOK NOW") was working correctly on mobile devices but completely disabled on desktop browsers. The root cause was a blanket CSS rule applying animation: none !important to all animations when the system-level prefers-reduced-motion: reduce accessibility setting was enabled—a common configuration for users with vestibular disorders or motion sensitivity.

The fix involved converting the animation from pure CSS keyframes to a JavaScript-driven opacity manipulation, making it immune to CSS animation media query kills while still respecting the user's accessibility preference through explicit JS logic.

Technical Details

The Problem

The staging site at staging.queenofsandiego.com serves its index file from S3 bucket s3://staging.queenofsandiego.com/ with CloudFront distribution ID XXXXXX (invalidation prefix: /index.html). The hero section animation was implemented using CSS keyframes:

@keyframes fadeInOut {
  0%, 10% { opacity: 1; }
  25%, 90% { opacity: 0; }
  100% { opacity: 0; }
}

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

However, at line 1734 of the staging HTML, a media query was unconditionally killing all animations:

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

This rule was overly broad. While it's important to respect accessibility preferences, the implementation was using a hammer when we needed a scalpel. The issue manifested on macOS and Linux machines where the system-level "Reduce motion" accessibility setting was enabled (Settings → Accessibility → Display → Reduce motion), which automatically sets the prefers-reduced-motion media feature.

Mobile devices (iOS/Android) typically ship with this setting disabled by default, explaining why the animation worked on mobile but not desktop.

The Solution

We migrated the animation logic from CSS to vanilla JavaScript, allowing us to:

  1. Query the prefers-reduced-motion setting programmatically via the Media Queries Level 5 API
  2. Gracefully disable animation only when explicitly requested by the user
  3. Maintain the same visual effect while being immune to CSS animation blanket rules

The implementation in /tmp/staging-index.html (and subsequently deployed to S3) includes:

const heroTexts = document.querySelectorAll('.hero-text-cycle');
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;

if (!prefersReducedMotion) {
  let currentIndex = 0;
  const texts = ['JADA', 'BOOK NOW'];
  
  setInterval(() => {
    heroTexts.forEach(el => {
      el.style.opacity = '0';
    });
    
    setTimeout(() => {
      heroTexts[currentIndex].style.opacity = '1';
      currentIndex = (currentIndex + 1) % texts.length;
    }, 300);
  }, 8000);
} else {
  // User prefers reduced motion: show primary text permanently
  heroTexts[0].style.opacity = '1';
}

This approach:

  • Detects the preference at page load via window.matchMedia()
  • Only runs the cycling animation if motion is not reduced
  • Gracefully falls back to showing the primary CTA ("JADA") when motion is disabled
  • Uses CSS transitions for the opacity change (smooth 300ms fade) while the timing is controlled by JavaScript

Infrastructure Changes

No infrastructure changes were required. The fix was purely an HTML and JavaScript modification within the already-deployed staging file.

Deployment Process

  1. Local testing: Modified /tmp/staging-index.html locally and verified animation behavior on both reduced-motion and standard configurations
  2. S3 upload: Pushed updated file to s3://staging.queenofsandiego.com/index.html
  3. CloudFront invalidation: Issued cache invalidation for /index.html on the staging distribution to ensure edge locations serve the updated file within seconds

Key Decisions

Why JavaScript Instead of CSS Animations?

CSS animations are elegant for simple cases, but they're subject to browser-level and OS-level media queries that can't be selectively disabled. The prefers-reduced-motion media feature is a useful accessibility tool, but a blanket animation: none !important rule is too aggressive—it doesn't distinguish between decorative animations (which should be disabled) and critical UX patterns (which users might still want).

By moving the logic to JavaScript, we can make a nuanced decision: "Is this animation essential to the user experience or purely decorative?" In this case, the hero section cycling is supplementary—showing the primary CTA ("JADA") without animation is still functional.

Why Not Just Fix the Media Query?

We could have changed the prefers-reduced-motion rule to be more selective:

@media (prefers-reduced-motion: reduce) {
  .hero-text-cycle {
    animation: none !important;
  }
}

However, this still doesn't solve the broader problem: CSS animations are inherently affected by media query rules. The JavaScript approach is more maintainable because future developers won't accidentally break this behavior by modifying CSS rules.

Verification and Testing

To verify the fix works across both configurations:

  • Reduced motion enabled: macOS System Settings → Accessibility → Display → Enable "Reduce motion" → Reload page → Verify hero text shows "JADA" without cycling
  • Reduced motion disabled: Disable the setting → Reload page → Verify cycling animation resumes (JADA → BOOK NOW → JADA...)
  • Mobile verification: iOS Settings → Accessibility → Motion → Enable "Reduce Motion" → Open site → Verify behavior matches desktop

Related Infrastructure Context

This staging file also includes pricing tier logic for event bookings managed via Google Apps Script backend. The events served through this staging infrastructure include:

  • Brandi Carlile
  • Buddy Guy
  • Bonnie Raitt
  • Mariachi USA
  • Gipsy Kings

Each event has its own CloudFront distribution and S3 bucket, but the main homepage hero animation was consolidated in the staging index file for consistency.

What's Next

  • Monitor analytics to confirm the animation is rendering consistently across all desktop browsers and OS configurations
  • Apply the same JavaScript animation pattern to other hero sections across event-specific subdomains if similar