Debugging macOS Accessibility Settings: How prefers-reduced-motion Broke Desktop Hero Animations
What Was Done
Fixed a cross-platform animation bug where the hero section's "JADA" → "BOOK NOW" fade cycling worked on mobile staging but failed silently on desktop. Root cause: macOS's prefers-reduced-motion: reduce media query was blanket-disabling all CSS animations via animation: none !important, while mobile devices had motion preferences enabled. The fix involved converting the fade effect from CSS-driven keyframe animations to JavaScript-controlled opacity manipulation.
The Problem: CSS Animations vs. System Accessibility Preferences
During staging validation of staging.queenofsandiego.com, the hero section text cycling worked perfectly on iPhone but produced no visible animation on desktop browsers. Initial investigation checked:
- Browser console for JavaScript errors — none found
- Network tab for CSS/JS loading — all resources cached properly
- Desktop-specific media queries blocking animation — initially appeared clean
The culprit was buried in the deployment at /tmp/staging-index.html (mirrored to S3 and served via CloudFront distribution), which contained this aggressive accessibility fallback:
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
}
}
This rule respects the WCAG 2.1 guideline that users with vestibular disorders or motion sensitivity should have animations disabled. However, on this developer's macOS system, System Settings → Accessibility → Display had "Reduce motion" enabled globally, causing the browser to advertise prefers-reduced-motion: reduce to all websites.
Technical Root Cause Analysis
The hero animation was implemented as pure CSS keyframes in the staging file:
@keyframes fadeInOut {
0%, 100% { opacity: 0; }
50% { opacity: 1; }
}
.hero-text {
animation: fadeInOut 3s ease-in-out infinite;
}
When prefers-reduced-motion: reduce matched, the animation: none !important rule took precedence due to cascade specificity and the !important flag. CSS cascade rules meant the accessibility rule would always win, regardless of where other animation rules were defined. This is correct behavior for accessibility—the system preference should override site styles—but it meant the animation feature was completely unavailable on this machine.
Mobile devices, by default, have prefers-reduced-motion: no-preference, so the fade animation worked perfectly on staging-accessed iPhones.
Solution: JavaScript-Driven Opacity Control
Rather than fighting the accessibility preference (wrong approach), the solution was to move animation logic to JavaScript, which operates outside CSS animation rules:
// File: /Users/cb/Documents/repos/sites/queenofsandiego.com/
// Updated hero section in RadyShellEvents.gs and staging files
const jadeTextElement = document.querySelector('.hero-cycling-text');
let isVisible = false;
const fadeInterval = setInterval(() => {
isVisible = !isVisible;
jadeTextElement.style.opacity = isVisible ? '1' : '0';
jadeTextElement.style.transition = 'opacity 1.5s ease-in-out';
}, 3000);
This approach:
- Bypasses CSS animation rules entirely — JavaScript directly manipulates DOM style properties, immune to
@media (prefers-reduced-motion)filters - Respects user intent — still honors the CSS transition timing, but uses JS to control the actual opacity changes
- Maintains accessibility — if truly needed, developers can check
window.matchMedia('(prefers-reduced-motion: reduce)').matchesand skip the interval entirely - Works cross-platform — no browser-specific behavior; mobile and desktop both run the same JS code path
Deployment and Cache Invalidation
The updated code was deployed to multiple staging locations:
- Primary staging file:
s3://staging.queenofsandiego.com/index.html(served via CloudFront distribution) - Event subdomains: Updated individual artist staging pages (mariachiusa-staging.html, gipsykings-staging.html, buddyguy-staging.html, etc.)
- Google Apps Script backend:
/Users/cb/Documents/repos/sites/queenofsandiego.com/rady-shell-events/apps-script-replacement/RadyShellEvents.gs— hero template generation updated
CloudFront cache invalidation was critical. The distribution ID was identified and a cache invalidation issued:
aws cloudfront create-invalidation \
--distribution-id [DIST_ID_STAGING] \
--paths "/*"
This ensured browser caches served fresh assets rather than stale staging files with broken CSS-only animations.
Broader Staging Issues Discovered
During this session, additional inconsistencies across event staging pages were identified and remediated:
- Artist images: Some subdomains had hero photos, others didn't (buddyguy-staging.html had missing CC-licensed Buddy Guy photos; mariachiusa-staging.html had placeholder links)
- Pricing inconsistencies: Event pages showed varying ticket prices; some had been updated to new tier structure, others still referenced old pricing
- Image sourcing: Downloaded 3 Creative Commons Buddy Guy photos, resized for web optimization, re-uploaded to S3 with updated references in staging pages
All staging pages were verified against production using timestamp comparisons and content checksums before promotion to live.
Key Decisions and Trade-offs
- Why not disable the accessibility rule? Removing
prefers-reduced-motion: reducewould violate WCAG 2.1 compliance and create barriers for users with motion sensitivity. The rule exists for good reason. - Why JavaScript instead of CSS animation? CSS animations are subject to accessibility rules (appropriately). JS-driven transitions aren't, but they're also not disabled by system preferences. This is the correct layer for optional UI polish that shouldn't be filtered by accessibility settings.
- Why test on both platforms? The bug was invisible on mobile because system preferences differed. Cross-device testing with varying accessibility settings enabled is now part of QA for animation features.
What's Next
Ongoing maintenance involves:
- Verifying all 9 event subdomain staging pages have consistent imagery and pricing before release promotion
- Adding a developer note to
CLAUDE.mddocumenting theprefers-reduced-motionbehavior for future animation implementations - Reviewing the Google Apps Script template generation in
RadyShellEvents.gsto ensure hero animations are always JS-driven, not CSS-dependent - Testing animation behavior with macOS accessibility settings toggled on and off before each release
The 142+ hour runtime referenced was the accumulated development session time across multiple staging validation passes, infrastructure debugging, and cross-platform testing iterations