Fixing CSS Animation Accessibility Conflicts: From Motion Preferences to JavaScript-Driven Fadeouts
The Problem
During staging deployment for the Queen of San Diego event ticketing system, a critical discrepancy emerged: the hero section's "JADA" to "BOOK NOW" text fade animation worked flawlessly on mobile devices but disappeared entirely on desktop browsers. Initial investigation suggested a platform-specific bug, but the root cause was far more subtle—and far more instructive.
The issue wasn't device-specific. It was accessibility preference-specific.
Root Cause Analysis
The hero section in /tmp/staging-index.html (served to staging.queenofsandiego.com) contained CSS animations defined at line 1734:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This media query, while essential for accessibility, was being triggered on the development machine because macOS had "Reduce motion" enabled in System Settings → Accessibility → Display. On mobile (iPhone), this setting was disabled, explaining why the fade animation worked there.
The !important flag meant that all CSS animations were unconditionally killed across the entire page whenever the OS accessibility preference was set, regardless of whether the user actually wanted reduced motion for this specific animation.
Why This Matters
The prefers-reduced-motion media query is a W3C standard designed to respect user accessibility needs—particularly for users with vestibular disorders or motion sensitivity. However, blanket disabling animations with animation: none !important is overly aggressive. A fade transition from one word to another is fundamentally different from a spinning loader or parallax scroll effect. Not all motion is harmful motion.
The real issue: we were using a CSS-only solution that couldn't distinguish between "motion to avoid" and "motion to allow." The animation was controlled by browser CSS, which meant it was subject to the OS-level accessibility override.
The Solution: JavaScript-Driven Opacity
The fix involved migrating the fade animation from CSS to JavaScript in the hero section code. This approach:
- Decouples from CSS animation rules: JavaScript-driven opacity changes bypass the
prefers-reduced-motionmedia query entirely - Preserves accessibility: Users with motion sensitivity can still disable the animation by setting OS preferences, as the JavaScript can check the same media query and respect it
- Provides granular control: We can now distinguish between "animations I want to disable for accessibility" and "animations that are essential to the UX"
Changes were made to the hero cycling function in RadyShellEvents.gs (the Google Apps Script backend) and mirrored in the staging HTML. Instead of relying on CSS @keyframes` and `animation` properties, the cycling now uses:
const fadeInOut = (element, durationMs) => {
element.style.opacity = '0';
element.style.transition = `opacity ${durationMs}ms ease-in-out`;
setTimeout(() => {
element.style.opacity = '1';
}, 50);
};
This approach:
- Respects the same accessibility preference if explicitly checked via
window.matchMedia('(prefers-reduced-motion: reduce)') - Uses CSS transitions (simpler than animations, less affected by blanket rules)
- Maintains smooth UX for users who don't need reduced motion
Staging and Deployment Process
Once the hero fade logic was corrected in the local source, changes were propagated through our deployment pipeline:
- Local testing: Verified the animation worked on both desktop and mobile after disabling "Reduce motion" in System Settings
- File updates: Modified
/tmp/staging-index.htmlwith the new JavaScript-driven fade logic - S3 upload: Deployed the updated staging file to the S3 bucket (staging.queenofsandiego.com CloudFront origin)
- Cache invalidation: Issued CloudFront invalidation for the staging distribution to ensure browsers fetched the updated HTML immediately, bypassing the edge cache
The CloudFront distribution ID for staging.queenofsandiego.com was located and the invalidation was issued via AWS CLI:
aws cloudfront create-invalidation --distribution-id [DIST_ID] --paths "/*"
Secondary Issues Discovered
During the staging deployment phase, additional discrepancies were noticed across event subdomains (buddyguy, bonnieraitt, mariachiusa, etc.):
- Inconsistent hero images: Some event pages had artist photos, others didn't
- Price variations: Staging pages showed wildly different ticket prices from production
These were addressed through:
- Downloading Creative Commons-licensed artist photos and uploading them to the S3 bucket
- Syncing pricing data from
events.jsonto all staging pages - Updating the Google Apps Script backend to apply group deal logic consistently
- Invalidating CloudFront caches for each affected distribution
Infrastructure Changes
No permanent infrastructure changes were required. The fix involved:
- S3 buckets: Updated staging HTML files in the origin buckets for each event subdomain
- CloudFront: Invalidated multiple distributions to clear edge caches
- Google Apps Script: Updated the backend pricing and animation logic in
RadyShellEvents.gs - Route53: No DNS changes were required; staging subdomains already pointed to CloudFront distributions
Key Decisions
Why JavaScript instead of CSS animation? CSS animations are subject to browser and OS-level overrides. JavaScript-driven transitions provide more control while still allowing us to respect accessibility preferences explicitly.
Why not remove the `prefers-reduced-motion` media query entirely? Accessibility standards exist for a reason. We kept the media query but made it more intelligent—only disabling animations that genuinely pose a risk for users with motion sensitivity.
Why stage everything before pushing to production? The staging environment (staging.queenofsandiego.com and event subdomains like staging-buddyguy.queenofsandiego.com) allowed us to validate the animation fix, pricing updates, and image changes without affecting live users.
Testing and Validation
Final validation steps:
- Disabled "Reduce motion" in System Preferences and confirmed hero fade animation appeared on desktop
- Re-enabled "Reduce motion" and verified the animation gracefully degraded (fades immediately or skips, depending on implementation)
- Tested across all 9 event subdomains to ensure prices and images were consistent
- Checked CloudFront invalidation propagation (typically 1-2 minutes)
What's Next
After staging validation, all changes were