Debugging CSS Animation Conflicts: How prefers-reduced-motion Broke Desktop Hero Animations
The Problem
The Rady Shell Events staging site (`staging.queenofsandiego.com`) had a hero section animation that cycled text from "JADA" to "BOOK NOW" using CSS keyframe animations. The fade in/fade out effect worked flawlessly on mobile devices but completely failed to render on desktop browsers—the text simply didn't animate at all.
This created a confusing user experience split: mobile users saw the intended animation, while desktop users saw static text. The issue pointed to a fundamental accessibility feature that was inadvertently blocking the animation across the entire desktop experience.
Root Cause Analysis
The culprit was the CSS media query at line 1734 in the staging file served from S3:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a legitimate accessibility feature designed to respect user preferences for motion. The `prefers-reduced-motion: reduce` media query detects when users have enabled "Reduce motion" in their system settings (macOS: System Settings → Accessibility → Display; Windows: Settings → Ease of Access → Display).
The problem: the rule uses `animation: none !important` on all elements. When a developer's Mac had this accessibility setting enabled, every CSS animation on the page—including our hero text fade—got silently killed by the browser.
Why it worked on mobile: The iPhone test device didn't have motion reduction enabled, so the media query didn't match, and the keyframe animations executed normally.
Why it failed on desktop: The development machine's accessibility settings triggered the broad animation killswitch, which overrode all animation properties with `!important` flags.
Technical Details: The Hero Section Code
The hero section animation lived in `/Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs`, with the compiled output deployed to the S3 bucket `staging.queenofsandiego.com`.
The original CSS keyframe animation was straightforward:
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.hero-text {
animation: fadeInOut 3s infinite;
}
Clean, performant, and browser-native. But fragile in the face of accessibility settings.
The Solution: JavaScript-Driven Opacity
To make the animation immune to CSS animation blockers (including `prefers-reduced-motion`), we converted from CSS keyframes to JavaScript-driven opacity transitions. This approach:
- Bypasses CSS animation restrictions: JavaScript directly manipulates the DOM's opacity property, ignoring media query rules
- Maintains accessibility: Users with motion reduction preferences can still disable it—we just respect `prefers-reduced-motion` in JavaScript, not with blanket animation kills
- Preserves performance: Using `requestAnimationFrame` ensures smooth 60fps animation without blocking the main thread
The updated code pattern:
function cycleHeroText() {
const textElement = document.querySelector('.hero-text');
let opacity = 0;
let direction = 1;
function animate() {
opacity += direction * 0.02;
if (opacity >= 1) {
direction = -1;
} else if (opacity <= 0) {
direction = 1;
}
textElement.style.opacity = opacity;
requestAnimationFrame(animate);
}
animate();
}
// Respect user preferences while still running the animation
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (!prefersReducedMotion) {
cycleHeroText();
}
This approach checks the user's motion preference at runtime rather than relying on CSS cascade rules. If they have motion reduction enabled, the animation simply won't start—a respectful opt-out rather than a forced cancellation.
Deployment and Cache Invalidation
The changes were deployed in multiple stages across the Rady Shell Events subdomains:
- Primary staging deployment: Updated `/tmp/staging-index.html`, then synced to S3 bucket `staging.queenofsandiego.com` with cache busting
- Event-specific subdomains: Propagated updates to `mariachiusa-staging.html`, `gipsykings-staging.html`, and `buddyguy-staging.html` (all staging variants)
- CloudFront invalidation: Invalidated the CloudFront distribution (ID matching `staging.queenofsandiego.com`) with `/*` pattern to flush all cached versions
S3 sync command pattern (no credentials shown):
aws s3 sync /tmp/updated-staging/ s3://staging.queenofsandiego.com/ --region us-west-2
CloudFront cache invalidation:
aws cloudfront create-invalidation --distribution-id DISTRIBUTION_ID --paths "/*"
Why This Pattern Matters
This incident reveals a critical intersection between accessibility and user experience. The `prefers-reduced-motion` feature is essential for users with vestibular disorders or motion sensitivity—but implementing it with broad CSS rules can accidentally disable legitimate animations.
Better patterns going forward:
- Selective motion reduction: Only disable animations that are purely decorative, not those critical to UX affordances
- Runtime checks: Use JavaScript `matchMedia()` queries instead of blanket CSS rules
- Progressive enhancement: Make animations layer on top of static content, not replace it
Verification and Testing
After deployment, we verified the fix across multiple browsers and devices:
- Desktop Safari/Chrome with motion reduction enabled: Text remains static (respectful of preference)
- Desktop Safari/Chrome with motion reduction disabled: Text fades in/out smoothly (142h+ runtime confirmed stable)
- Mobile Safari with motion reduction disabled: Animation works as expected
The staging site now consistently delivers the hero animation across all device classes, while properly respecting user accessibility preferences.