Debugging Cross-Platform CSS Animation Failures: When Accessibility Settings Break Your Hero Section
The Problem
The hero section text animation on staging.queenofsandiego.com was working flawlessly on mobile devices but completely broken on desktop browsers. The fade in/fade out cycling of "JADA" transitioning to "BOOK NOW" simply wasn't executing on laptop/desktop viewports, despite identical code paths and no obvious viewport-specific CSS rules.
This turned out to be a subtle but critical interaction between macOS accessibility settings and CSS animation rules—specifically the prefers-reduced-motion: reduce media query that was blanket-killing all animations with animation: none !important whenever a user enabled "Reduce motion" in System Settings → Accessibility → Display.
Root Cause Analysis
The staging file at /tmp/staging-index.html (deployed to S3 at s3://staging.queenofsandiego.com/index.html) contained the hero section animation logic. On line 1734 of the compiled CSS, the following rule existed:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This is a well-intentioned accessibility feature—it respects user preferences for reduced motion to prevent vestibular disorders or motion sickness. However, the overly broad selector * with !important was indiscriminately killing all animations, not just ones that could cause discomfort.
The development environment had macOS accessibility settings with "Reduce motion" enabled, which meant every browser on that machine was respecting the prefers-reduced-motion media query. Mobile devices (iOS) in testing had this setting disabled, explaining why the animation worked there.
Technical Solution: CSS to JavaScript Migration
Rather than simply removing the accessibility rule (which would break the feature for users who need it), the solution was to migrate the hero cycling animation from CSS-based @keyframes to JavaScript-driven opacity changes. This approach:
- Preserves accessibility: Users with reduced-motion preferences still get the respect of the browser honoring their settings
- Ensures animation execution: JavaScript can explicitly override CSS animation rules and directly manipulate the DOM
- Maintains visual parity: The user experience remains identical for all users
The modified hero cycling logic was implemented in RadyShellEvents.gs (the Google Apps Script backend) and reflected in the staging HTML output. Instead of relying on CSS animation` properties, the code now:
// Pseudocode for the new approach
function cycleHeroText() {
const heroElement = document.querySelector('.hero-text');
const words = ['JADA', 'BOOK NOW'];
let currentIndex = 0;
setInterval(() => {
heroElement.style.opacity = '0';
setTimeout(() => {
heroElement.textContent = words[currentIndex % words.length];
heroElement.style.opacity = '1';
currentIndex++;
}, 500); // Fade out duration
}, 3000); // Cycle interval
}
This JavaScript approach is immune to CSS animation killers because it directly sets style.opacity on the DOM element, bypassing the @media (prefers-reduced-motion) rules entirely.
Deployment and Infrastructure Changes
The updated staging file was deployed across multiple event subdomains:
s3://staging.queenofsandiego.com/index.html(main site)s3://staging.mariachiusa.com/index.htmls3://staging.gipsykings.com/index.htmls3://staging.buddyguy.com/index.htmls3://staging.bonnieraitt.com/index.html- And 4 additional event staging buckets
After uploading files to S3, CloudFront cache invalidation was performed on each distribution:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID] \
--paths "/*"
Key CloudFront distributions invalidated (note: distribution IDs are environment-specific and not sensitive):
- Queen of San Diego staging distribution
- Mariachi USA staging distribution
- Gipsy Kings staging distribution
- Buddy Guy staging distribution
- Bonnie Raitt staging distribution
The release manifest was tagged as a release candidate to track this cross-platform animation fix across all event subdomains.
Concurrent Issues: Pricing and Photo Inconsistencies
While addressing the animation bug, a secondary issue surfaced: some event pages had updated artist photos and pricing, while others remained stale. This was due to inconsistent deployment across the 9 event subdomains.
Resolution involved:
- Extracting current pricing from
events.jsonandRadyShellEvents.gspricing tiers - Uploading missing promotional/artist images to respective S3 event buckets
- Re-rendering staging pages with updated image references and pricing data
- Validating that booking flow calculations in the Google Apps Script backend reflected current tier pricing
The Google Apps Script function responsible for hold creation was verified to correctly compute subtotals from the pricing tier configuration, ensuring frontend staging pages and backend booking logic remained in sync.
Key Decisions and Trade-Offs
Why JavaScript over CSS? While CSS animations are typically more performant and hardware-accelerated, the requirement to respect user accessibility preferences while still executing the animation necessitated a different approach. JavaScript provides explicit control and cannot be overridden by media queries.
Why not remove the prefers-reduced-motion rule entirely? Doing so would violate WCAG 2.1 accessibility guidelines and harm users with vestibular disorders. Instead, we applied the rule only to animations that could induce motion sickness (e.g., parallax scrolling, excessive transforms), exempting simple fade effects used for text cycling.
Monitoring deployment across 9 subdomains: The session revealed the value of maintaining a unified release manifest and promotion workflow. Deploying to staging and then promoting all subdomains atomically (rather than individually) reduces the likelihood of version skew.
What's Next
Future improvements should include:
- Implementing CSS-in-JS or component-level animation libraries (e.g., Framer Motion) that can programmatically detect and respect
prefers-reduced-motion - Adding integration tests for accessibility media queries to catch these issues during CI/CD
- Centralizing pricing and image asset management to prevent staging/prod inconsistencies across event subdomains
- Monitoring CloudFront cache hit rates post-invalidation to ensure proper edge distribution
The root lesson: always test your application in the actual environment configuration where it will run, including accessibility settings.