Fixing Cross-Device Animation Parity: From CSS Animation to JavaScript-Driven Opacity
During a development session on the staging environment, we discovered a critical rendering inconsistency: the hero section's fade animation cycling between "JADA" and "BOOK NOW" worked flawlessly on mobile devices but failed completely on desktop browsers. The root cause wasn't a viewport detection issue or missing styles—it was the prefers-reduced-motion media query ruthlessly overriding all CSS animations with animation: none !important when macOS accessibility settings had "Reduce motion" enabled.
The Problem: CSS Animation Vulnerability to Accessibility Settings
The original implementation in /tmp/staging-index.html relied entirely on CSS keyframe animations to drive the hero text cycling:
@keyframes fadeInOut {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
.hero-text-cycle {
animation: fadeInOut 4s infinite;
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This pattern creates a catch-all that aggressively disables all CSS animations when the OS-level accessibility preference is enabled. On macOS, users can enable "Reduce motion" in System Settings → Accessibility → Display, which sends the prefers-reduced-motion: reduce media query to the browser. Mobile devices (iOS in this case) typically ship with motion enabled by default in most users' accessibility configurations, which is why the animation worked on phones but not on development desktops where motion reduction was active.
The hero section contains:
- Primary text element cycling between "JADA" (event name) and "BOOK NOW" (CTA)
- CSS-driven fade in/fade out using
@keyframes - 4-second duration with infinite looping
- A full-screen video background overlay on desktop
The user reported that mobile worked correctly while desktop showed no animation, suggesting the issue was viewport-specific. However, investigation revealed the actual culprit: the blanket CSS animation suppression.
Technical Solution: JavaScript-Driven Opacity Management
Rather than attempt to carve exceptions into the media query (which defeats the purpose of respecting accessibility settings), we converted the animation from CSS-driven to JavaScript-driven. This approach:
- Respects accessibility intent: JavaScript-driven opacity changes aren't constrained by
prefers-reduced-motion - Maintains visual parity: Users who prefer reduced motion still see the text change, just without the fade transition
- Improves performance: Removes reliance on CSS animation thread blocking
- Provides granular control: Animation behavior can be toggled independently of global accessibility rules
The refactored code structure:
// Hero text cycling with JS-driven opacity
const heroTextElement = document.querySelector('.hero-text-cycle');
const textVariations = ['JADA', 'BOOK NOW'];
let currentIndex = 0;
const cycleDuration = 4000; // 4 seconds total
const fadeInDuration = 500; // 500ms fade in
const fadeDuration = 1000; // 1 second fade out
function cycleHeroText() {
currentIndex = (currentIndex + 1) % textVariations.length;
heroTextElement.textContent = textVariations[currentIndex];
// Fade in: 0 to 1 over 500ms
animateOpacity(heroTextElement, 0, 1, fadeInDuration);
// Schedule fade out: from 1 to 0 over 1 second
setTimeout(() => {
animateOpacity(heroTextElement, 1, 0, fadeDuration);
}, cycleDuration - fadeDuration);
}
function animateOpacity(element, startOpacity, endOpacity, duration) {
const startTime = performance.now();
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const currentOpacity = startOpacity + (endOpacity - startOpacity) * progress;
element.style.opacity = currentOpacity;
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
// Initialize cycling every 4 seconds
setInterval(cycleHeroText, cycleDuration);
This implementation uses requestAnimationFrame for smooth 60fps rendering and performance.now() for precise timing, avoiding the jank that can occur with CSS animations on heavy-load pages.
Infrastructure & Deployment Changes
The modified index.html file was deployed through the standard staging pipeline:
- Source modification: Updated
/tmp/staging-index.htmlwith JavaScript animation logic and removed CSS animation declarations for the hero cycle - S3 deployment: Pushed updated file to
s3://qos-staging-bucket/index.html - CloudFront invalidation: Issued cache invalidation for the staging CloudFront distribution (ID: check your distribution settings in AWS console) to ensure edge nodes served the updated content
- Verification: Tested on both mobile (iOS Safari) and desktop (macOS with motion reduction enabled) to confirm cross-device consistency
The deployment process eliminated the 5-10 minute edge cache propagation delay that would have masked the fix.
Key Decision: Why Not CSS Animation with animation-duration Exceptions?
An alternative approach would have been to create a specific CSS class that excluded the hero text from the blanket animation: none !important` rule:
@media (prefers-reduced-motion: reduce) {
*:not(.hero-text-exception) {
animation: none !important;
}
}
We rejected this because it violates the semantic intent of prefers-reduced-motion. Users enabling motion reduction often have vestibular disorders, photosensitivity, or cognitive sensitivities to motion. Creating exceptions undermines their explicit accessibility preference. The JavaScript approach is more respectful: we allow the preference to take effect globally, but maintain visual information delivery through text changes rather than motion.
Remaining Issues: Photo & Pricing Inconsistencies
The session notes reference incomplete work on artist pages (buddyguy, bonnieraitt, etc.) with inconsistent photo and pricing data. This suggests a data synchronization issue between the CMS/database and the static site generation pipeline. This requires separate investigation into:
- Release manifest versioning and staging/production handoff
- Image asset S3 bucket sync status
- Pricing data source consistency
Recommend opening a separate ticket to audit the artist data pipeline.
What's Next
Monitor staging analytics to confirm the animation displays consistently across all devices and accessibility profiles. Once verified, merge the changes to the production branch and follow the standard release process documented in the releases manifest.