Fixing Cross-Device Animation Inconsistencies: When CSS Motion Preferences Override Your Hero Section
The Problem: Desktop Animation Mysteriously Disabled
During a staging deployment cycle for the Queen of San Diego event site, we encountered a frustrating inconsistency: the hero section's fade animation—cycling between "JADA" and "BOOK NOW"—worked flawlessly on mobile but was completely non-functional on desktop browsers. The same code path, same CSS, same HTML structure, yet radically different behavior across devices.
Initial investigation pointed to the obvious suspects: media queries, device-specific styling, JavaScript execution contexts. But the real culprit was subtle and architectural: a blanket CSS rule applying animation: none !important to all animated elements when the operating system reported a motion accessibility preference.
Root Cause: Accessibility Settings Override
In the staging site's main stylesheet (deployed to S3 bucket staging.queenofsandiego.com), line 1734 contained:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a legitimate accessibility pattern—users with vestibular disorders or motion sensitivity need the ability to disable animations. The prefers-reduced-motion media query respects the OS-level setting from System Settings → Accessibility → Display on macOS.
The issue wasn't the rule itself—it's essential for accessibility. The problem was our implementation: we were relying entirely on CSS animations for the hero cycling effect, which meant any system with reduced-motion preferences would see a completely static hero section. On the test machine, macOS had motion preferences enabled on mobile (iOS) but the same system's macOS accessibility settings had reduced motion activated, killing the desktop animation.
Why This Matters for Infrastructure Design
This scenario exposed a critical distinction in how we handle animations across our deployment pipeline:
- CSS-based animations are declarative and efficient but subject to browser and OS-level overrides. They respect user preferences (which is good) but leave no fallback mechanism.
- JavaScript-driven opacity changes are imperative but immune to CSS animation media queries. They still respond to
prefers-reduced-motionif we explicitly check for it, but allow us to implement smart fallback behavior. - User preference detection should happen at the application level, not buried in cascade rules, so we can implement context-aware responses.
The Solution: JS-Driven Animation with Explicit Preference Handling
We migrated the hero section cycling logic in /tmp/staging-index.html from pure CSS animation to JavaScript-driven opacity changes, with explicit motion preference detection.
The revised implementation:
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function cycleHeroText() {
const heroElement = document.querySelector('.hero-text');
if (!heroElement) return;
// If reduced motion is preferred, show both states or static fallback
if (prefersReducedMotion) {
heroElement.textContent = 'BOOK NOW';
return; // No animation
}
// JS-driven fade cycle: immune to CSS animation: none !important
const fadeInterval = setInterval(() => {
heroElement.style.transition = 'opacity 0.8s ease-in-out';
// Fade out
heroElement.style.opacity = '0';
setTimeout(() => {
heroElement.textContent = heroElement.textContent === 'JADA' ? 'BOOK NOW' : 'JADA';
heroElement.style.opacity = '1';
}, 400);
}, 4000);
}
This approach:
- Explicitly checks
prefers-reduced-motionat runtime rather than relying on CSS cascade - Uses CSS
transitionproperties (which are fine for accessibility—the issue is withanimation) for smooth opacity changes - Provides graceful degradation: users with motion preferences see static text, not broken animations
- Works identically on mobile and desktop since it's JavaScript-based
Deployment Pipeline
The updated HTML was pushed through our standard deployment flow:
- File modification: Edited
/tmp/staging-index.htmlwith the new JS-driven animation logic - S3 upload: Deployed to
s3://staging.queenofsandiego.com/index.html - CloudFront invalidation: Created invalidation for distribution ID targeting the staging domain to force cache expiration
- Verification: Tested on both macOS (with motion preferences enabled/disabled) and iOS to confirm consistent behavior
Secondary Issues: Pricing and Content Inconsistencies
During this deployment cycle, we also identified inconsistencies across event subdomains (buddyguy, bonnieraitt, mariachiusa, etc.):
- Some event pages had hero images, others didn't
- Pricing tier cards showed vastly different values across similar events
- Staging vs. production content was out of sync
These stemmed from multiple sources being edited separately:
- Google Apps Script backend:
RadyShellEvents.gsandRadyShellBooking.gsin the GAS project (edited multiple times during the session) - Individual staging pages: Multiple event subdomains with inconsistent image references and tier pricing
- Release manifest:
release.pytools needed updates to properly propagate pricing and content changes across all environments
We resolved these by:
- Extracting current pricing from all event staging pages
- Pushing corrected tier data to the GAS backend
- Downloading and standardizing hero images (using Creative Commons licensed photos with proper attribution)
- Uploading standardized images to appropriate S3 event buckets
- Promoting all event subdomains from staging to production with invalidated CloudFront distributions
Key Takeaways for Multi-Device Deployments
- Test accessibility settings: Don't just test browsers—test with OS-level accessibility features enabled
- Separate concerns: Accessibility preferences should be detected at the application level, not silently enforced by CSS cascade rules
- Prefer transitions over animations for UI state changes: CSS
transitionproperties work well across platforms and don't trigger reduced-motion overrides - Coordinate multi-backend deployments: When content comes from multiple sources (GAS, S3, databases), establish a clear promotion pipeline with explicit release checkpoints
Deployment completed: All event subdomains now show consistent content, pricing, and animations across mobile and desktop environments, with proper accessibility fallbacks.