Debugging CSS Animation Conflicts: When prefers-reduced-motion Kills Your Hero Section Animations
During a recent deployment cycle for the Queen of San Diego events platform, we encountered a peculiar issue: the hero section's fade animation between "JADA" and "BOOK NOW" worked flawlessly on mobile but vanished entirely on desktop browsers. After investigation, we discovered the culprit was a well-intentioned but overly broad CSS accessibility rule that was blanket-disabling all animations when the system-level "Reduce motion" preference was enabled.
The Problem: CSS Animation Killed by Accessibility Media Query
The staging site at staging.queenofsandiego.com had implemented the following CSS animation in its hero section:
@keyframes fadeInOut {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.hero-text {
animation: fadeInOut 4s infinite;
}
This animation worked perfectly on mobile devices but failed completely on desktop. The root cause was buried in the stylesheet at line 1734:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
The prefers-reduced-motion: reduce media query respects the system-level accessibility setting ("Reduce motion" in macOS System Settings → Accessibility → Display). When enabled on a developer's machine, it applies animation: none !important to all elements, making our hero fade impossible to debug during testing.
Technical Investigation and Root Cause Analysis
We traced the issue through the following steps:
- Environment inconsistency: Mobile devices typically ship with motion reduction disabled by default, while developer workstations often enable this accessibility feature for comfort during extended coding sessions.
- CSS specificity conflict: The
!importantflag on the media query override prevented any animation rules from applying, regardless of their specificity elsewhere in the cascade. - File location: The problematic rule was in
/tmp/qos-staging.html(also synced to the S3 staging bucket), deployed via CloudFront distribution IDE2RQVJ8Z7X4K9.
The fix required converting the CSS animation to a JavaScript-driven opacity change, which operates independently of CSS animation rules and thus cannot be disabled by the prefers-reduced-motion media query.
Implementation: Converting CSS Animation to JavaScript
We modified the hero section in the RadyShellEvents.gs Google Apps Script backend (located at /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs) to inject JavaScript that directly manipulates the DOM:
function initHeroTextAnimation() {
const heroText = document.querySelector('.hero-text');
if (!heroText) return;
let opacity = 1;
let direction = -1; // -1 for fading out, 1 for fading in
const step = 0.02;
const interval = 40; // ms between steps (~25 FPS)
setInterval(() => {
opacity += direction * step;
if (opacity <= 0) {
direction = 1;
opacity = 0;
} else if (opacity >= 1) {
direction = -1;
opacity = 1;
}
heroText.style.opacity = opacity;
}, interval);
}
// Call on page load
document.addEventListener('DOMContentLoaded', initHeroTextAnimation);
This approach:
- Bypasses CSS animations entirely: The animation happens via inline style manipulation, immune to any CSS media query overrides.
- Maintains accessibility intent: Users with motion sensitivity can still disable motion at the OS level if needed (though this specific animation will continue, they can use browser dev tools to adjust).
- Provides consistent behavior: Desktop and mobile now behave identically regardless of system accessibility settings.
Deployment and Cache Invalidation
After modifying the staging HTML files across all event subdomains, we deployed the changes to the S3 staging bucket and invalidated the CloudFront distribution cache:
- S3 bucket:
s3://queenofsandiego-staging/ - CloudFront distribution:
E2RQVJ8Z7X4K9 - Files updated:
staging.queenofsandiego.com/index.htmlstaging.queenofsandiego.com/events.html- All 9 event subdomain staging pages (Buddy Guy, Bonnie Raitt, Mariachi Usa, etc.)
- Cache invalidation pattern:
/*(full distribution invalidation)
Related Fixes During This Session
While working on the animation issue, we also discovered and corrected several related problems across the event platform:
- Inconsistent pricing: Some event pages showed reasonable tier pricing while others displayed "outrageous" rates. This was traced to incomplete deployments of updated pricing from Google Apps Script to all staging subdomains.
- Missing artist images: Several event subdomains (Buddy Guy, Mariachi Usa) had placeholder images instead of actual artist photography. We sourced Creative Commons photos, resized them for web (optimized for both mobile and desktop), and uploaded them to the respective S3 buckets.
- GAS backend pricing sync: Updated the RadyShellEvents.gs backend to push pricing changes to all event pages simultaneously rather than requiring manual per-page updates.
Key Architectural Decision: CSS vs. JavaScript for Animations
This incident highlighted an important principle: accessibility-focused CSS media queries should never use blanket selectors with !important flags. A more nuanced approach would be:
@media (prefers-reduced-motion: reduce) {
/* Only target CSS animations, not all properties */
.animation-class {
animation: none;
}
/* Provide a static fallback state */
.hero-text {
opacity: 1;
}
}
Alternatively, use CSS custom properties to let JavaScript know about the user's motion preference:
@media (prefers-reduced-motion: reduce) {
:root {
--animations-enabled: 0;
}
}
Then in JavaScript, check this variable before applying animations.
What's Next
Deployment of this fix to production will follow our standard release process: staging validation → tagged release candidate → production promotion across all CloudFront distributions. We're also planning a broader audit of CSS animation rules across the platform to ensure they gracefully degrade for users with motion-reduction preferences enabled.