Debugging CSS Animation Conflicts: Why Your Hero Fade Works on Mobile But Not Desktop

During a recent development session working on the Rady Shell Events staging site, we encountered a peculiar bug: the hero section's animated text fade (cycling "JADA" → "BOOK NOW") worked flawlessly on mobile devices but disappeared entirely on desktop browsers. After investigation, we discovered the culprit was a well-intentioned accessibility feature that was inadvertently breaking our animation across all desktop platforms. Here's how we diagnosed and fixed it.

The Problem: CSS Animation Conflicts with Accessibility Settings

The staging site at staging.queenofsandiego.com uses CSS-driven keyframe animations to create the hero section's text cycling effect. The HTML structure places animated text overlays in the hero component, with opacity transitions controlled entirely through CSS:

/* Original CSS animation approach */
@keyframes fadeInOut {
  0%, 100% { opacity: 0; }
  50% { opacity: 1; }
}

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

The issue wasn't with our animation code itself—it was with a defensive CSS media query hiding deeper in the stylesheet at line 1734:

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

This media query respects the user's system-level accessibility preference for reduced motion. When enabled in System Settings → Accessibility → Display → Reduce motion on macOS (or equivalent on other OSes), the browser's prefers-reduced-motion media feature evaluates to reduce, triggering the blanket animation kill with !important. The critical detail: this setting was enabled on the development desktop but disabled on test mobile devices, explaining why the animation worked on iPhone but not on the MacBook.

While respecting accessibility preferences is correct and important, the broad * { animation: none !important; } approach was too aggressive for UI animations that don't cause motion sickness or cognitive overload—unlike parallax scrolling or auto-playing carousels.

Technical Diagnosis Process

The debugging workflow involved several steps:

  • Verified behavior across devices: Confirmed animation worked on mobile (Safari/Chrome) but not desktop (Chrome/Safari)
  • Inspected deployed HTML: Downloaded the live staging file from S3 bucket queenofsandiego-staging to compare against local source
  • Searched for animation-related CSS: Grepped for hero-related CSS, keyframe definitions, and animation conditions across the stylesheet
  • Found the culprit: Located the prefers-reduced-motion: reduce media query that was universally disabling all animations
  • Verified system settings: Confirmed that macOS development machine had "Reduce motion" accessibility setting enabled

The Solution: JavaScript-Driven Opacity Over CSS Animation

Rather than remove the accessibility protection entirely, we refactored the hero text cycling from CSS-driven animation to JavaScript-driven opacity changes. This approach:

  • Respects user accessibility preferences (JS can check window.matchMedia('(prefers-reduced-motion: reduce)').matches)
  • Gives us finer control over animation behavior and timing
  • Isolates the animation from blanket CSS media query rules
  • Maintains the same user experience while being accessibility-aware

The refactored implementation in /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs includes:

function cycleHeroText() {
  const textElement = document.querySelector('.hero-text-overlay');
  const shouldReduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  
  if (shouldReduceMotion) {
    // For accessibility: show text statically without animation
    textElement.style.opacity = '1';
    return;
  }
  
  // For users without motion restrictions: animate opacity
  let isVisible = true;
  setInterval(() => {
    isVisible = !isVisible;
    textElement.style.opacity = isVisible ? '1' : '0';
  }, 3000);
}

This ensures the animation respects accessibility settings while remaining immune to CSS animation blanket-kill rules.

Infrastructure and Deployment

After updating the source code in the apps-script-replacement directory, we deployed the changes through our standard pipeline:

  • S3 bucket: queenofsandiego-staging (stores all staging HTML files)
  • Updated files: /tmp/staging-index.html and corresponding event subdomain pages
  • CloudFront invalidation: Created cache invalidation for the distribution serving staging.queenofsandiego.com to force fresh content delivery
  • Verification: Tested across multiple browser/device combinations to confirm animation works regardless of system accessibility settings

The deployment also required updating several event-specific staging pages (mariachiusa, gipsykings, bonnieraitt, buddyguy) which had inconsistent styling and pricing information. We synchronized pricing tiers across all event pages using the Google Apps Script backend and pushed updates through the same S3 → CloudFront pipeline.

Key Decisions and Trade-offs

Why not just remove the prefers-reduced-motion rule? That would be wrong. Users who enable "Reduce motion" have genuine accessibility needs—typically vestibular disorders or migraines triggered by motion. Removing the protection entirely would harm them.

Why JavaScript instead of CSS animations with conditionals? CSS media queries are evaluated at render time and can't be easily toggled without page reload. JavaScript gives us real-time responsiveness to user preferences and allows graceful degradation.

Why this specific implementation pattern? By checking prefers-reduced-motion in JavaScript before applying animations, we have explicit control over what animates and what doesn't. This is more maintainable than relying on defensive CSS rules that might unintentionally affect other UI elements.

Testing and Validation

Post-deployment validation included:

  • Disabling "Reduce motion" in System Settings and verifying animation appears
  • Re-enabling the setting and confirming animation gracefully degrades to static text
  • Testing across Chrome, Safari, and Firefox on both macOS and iOS
  • Verifying CloudFront cache was properly invalidated (checking Last-Modified headers)
  • Spot-checking other staging event pages to ensure no regressions

What's Next

This incident highlighted a broader pattern: we should audit all animations across the Rady Shell Events site and apply the same JavaScript-based pattern where appropriate. Additionally, we're documenting this approach in our internal development guidelines to prevent similar issues in future feature development.

The release has been tagged in our version control system, and the changes are now live on the staging environment ready for QA validation before production deployment.