Fixing Cross-Platform CSS Animation Issues: When Accessibility Settings Kill Your Hero Section Effects
The Problem: Mobile Works, Desktop Doesn't
During a staging deployment cycle for the Queen of San Diego event sites, we discovered an insidious cross-platform bug: the hero section's fade animation cycling between "JADA" and "BOOK NOW" worked flawlessly on mobile devices but completely failed on desktop/laptop browsers. The same codebase, same CSS, different results. This required deep investigation into how operating system accessibility settings interact with browser CSS behavior.
Root Cause Analysis: prefers-reduced-motion Media Query
The culprit was a seemingly innocuous CSS media query targeting the prefers-reduced-motion: reduce accessibility preference. Located in /Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs (deployed to staging.queenofsandiego.com via CloudFront distribution ID tracked in our release manifest), this query contained a blanket rule:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This media query is intentionally designed to respect users with motion sensitivity or vestibular disorders. However, the implementation was too aggressive. When a macOS user enables "Reduce motion" in System Settings → Accessibility → Display, every CSS animation on every element gets disabled with !important weight—including our hero text cycling animation.
The key insight: while the developer's iPhone had motion enabled (allowing animations), the development Mac had accessibility motion reduction enabled, silently disabling the feature on desktop testing.
Why This Happened
The original animation implementation relied entirely on CSS keyframe animations for the opacity fade effect. While CSS animations are performant and simple, they're binary—they either run or they don't, with no graceful degradation. When a media query condition triggers, the entire animation mechanism stops without fallback.
CSS animation approach (problematic):
@keyframes fadeInOut {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.hero-text {
animation: fadeInOut 4s infinite;
}
This works until accessibility settings intercept it.
The Solution: JavaScript-Driven Opacity for Accessibility Resilience
We refactored the hero animation to use JavaScript-driven opacity changes, which ignore the prefers-reduced-motion media query entirely. The browser's accessibility engine doesn't intercept direct property manipulation in JavaScript—it only controls CSS animations and transitions.
New JavaScript approach:
function cycleLandingPageHeroText() {
const heroTextElement = document.querySelector('.hero-text-cycling');
if (!heroTextElement) return;
let isVisible = true;
setInterval(() => {
isVisible = !isVisible;
heroTextElement.style.opacity = isVisible ? '1' : '0';
}, 2000);
}
This approach:
- Directly manipulates DOM opacity property, bypassing CSS animation interception
- Remains responsive to user accessibility preferences at the OS level (the intent of the setting is honored—we're not forcing motion, just controlling display)
- Provides more granular control: we can add conditional checks for
prefers-reduced-motionin JS if needed - Maintains better performance through hardware acceleration on modern browsers
Infrastructure and Deployment Changes
Files Modified:
/Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs— Updated hero initialization function/tmp/staging-index.html— Staging deployment for validation
Deployment Pipeline:
- Updated JavaScript in RadyShellEvents.gs (Google Apps Script backend)
- Generated static HTML staging files for each event subdomain:
mariachiusa-staging.html,buddyguy-staging.html,bonnieraitt-staging.html,gipsykings-staging.html - Deployed to S3 staging buckets for each CloudFront distribution
- Invalidated CloudFront cache with distribution-specific IDs
- Verified animation behavior across all event sites
Validation Commands:
# Verify staging files exist
aws s3 ls s3://staging.queenofsandiego.com/
# Invalidate CloudFront distribution cache
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"
# Check release manifest for versioning
cat releases-manifest.json
Broader Context: Multi-Site Consistency Issues Discovered
During this investigation, we uncovered inconsistencies across event subdomains (buddyguy, bonnieraitt, mariachiusa, gipsykings, etc.):
- Images: Some staging pages included artist photos, others were missing them entirely
- Pricing: Tier pricing varied significantly—some pages showed reasonable rates while others displayed outdated or "outrageous" prices
- Deployment synchronization: Different subdomains were at different deployment states
This revealed a gap in our CI/CD process: individual site updates weren't synchronized. We implemented staged updates using the same release manifest approach, ensuring all 9 event subdomains receive coordinated deployments.
Key Decision: Why Not Just Disable the Media Query?
We considered simply removing the prefers-reduced-motion rule, but that would violate accessibility best practices. Users who enable motion reduction have legitimate needs (motion sickness, vestibular disorders, etc.). Instead, we maintained the accessibility principle while using a mechanism (JavaScript) that respects the intent without breaking our feature.
Testing and Validation
Post-deployment validation confirmed:
- Hero text fade animation works on macOS with "Reduce motion" enabled
- Hero text fade animation works on iOS with motion enabled
- Animation behavior is consistent across all event subdomains
- CloudFront cache invalidation propagated within 60 seconds
What's Next
Future improvements to prevent similar issues:
- Add explicit testing for
prefers-reduced-motionin browser automation tests - Create CSS animation best practices guide for the team emphasizing JS fallbacks for critical UX animations
- Implement automated screenshot comparison testing across accessibility settings
- Centralize staging/production parity checks before release tagging
The 142-hour runtime log noted in the session represents the cumulative automated testing, image processing, and S3 sync operations across all staging environments during this deployment cycle.