Skip to main content
Beta: Front-End Checklist is currently in beta. Some issues are still being fixed. Thanks for your patience.

Provide alternatives to parallax effects

Parallax scrolling effects have reduced-motion alternatives or can be disabled by users.

Utilities
Quick take
Typical fix time 20 min
  • Disable parallax when prefers-reduced-motion is set
  • Parallax creates depth mismatch triggering vestibular disorders
  • Provide user toggle to disable motion effects site-wide
  • Ensure essential content is accessible without the effect
Why it matters: Parallax creates a mismatch between what the eyes see and what the inner ear senses—triggering dizziness, nausea, and vertigo for users with vestibular disorders.

Rule Details

Parallax effects can cause severe discomfort for users with vestibular disorders.

Code Example

/* ❌ Bad: Always applies parallax */
.hero {
  background-attachment: fixed;
  background-position: center;
  transform: translateZ(-1px) scale(2);
}
 
/* ✅ Good: Only when motion is acceptable */
@media (prefers-reduced-motion: no-preference) {
  .hero {
    background-attachment: fixed;
  }
}
 
/* Reduced motion: static background (default) */
.hero {
  background-attachment: scroll;
}

Why It Matters

Parallax creates a mismatch between what the eyes see and what the inner ear senses—triggering dizziness, nausea, and vertigo for users with vestibular disorders.

JavaScript Parallax Control

class ParallaxController {
  private prefersReducedMotion: boolean
 
  constructor() {
    this.prefersReducedMotion = window.matchMedia(
      '(prefers-reduced-motion: reduce)'
    ).matches
 
    // Listen for preference changes
    window.matchMedia('(prefers-reduced-motion: reduce)')
      .addEventListener('change', (e) => {
        this.prefersReducedMotion = e.matches
        this.toggle(!e.matches)
      })
  }
 
  init(elements: HTMLElement[]) {
    if (this.prefersReducedMotion) return
 
    elements.forEach(el => this.applyParallax(el))
    window.addEventListener('scroll', () => this.update(elements))
  }
 
  private applyParallax(element: HTMLElement) {
    // Parallax logic here
  }
 
  toggle(enabled: boolean) {
    document.body.classList.toggle('parallax-disabled', !enabled)
  }
}

React Parallax Component

interface ParallaxProps {
  children: React.ReactNode
  speed?: number // 0 to 1, default 0.5
  className?: string
}
 
function Parallax({ children, speed = 0.5, className }: ParallaxProps) {
  const [offset, setOffset] = useState(0)
  const prefersReducedMotion = useReducedMotion()
  const ref = useRef<HTMLDivElement>(null)
 
  useEffect(() => {
    if (prefersReducedMotion) return
 
    const handleScroll = () => {
      if (ref.current) {
        const rect = ref.current.getBoundingClientRect()
        setOffset(rect.top * speed)
      }
    }
 
    window.addEventListener('scroll', handleScroll, { passive: true })
    return () => window.removeEventListener('scroll', handleScroll)
  }, [speed, prefersReducedMotion])
 
  return (
    <div
      ref={ref}
      className={className}
      style={{
        transform: prefersReducedMotion ? 'none' : `translateY(${offset}px)`
      }}
    >
      {children}
    </div>
  )
}
 
function useReducedMotion() {
  const [prefersReduced, setPrefersReduced] = useState(false)
 
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)')
    setPrefersReduced(mediaQuery.matches)
 
    const handler = (e: MediaQueryListEvent) => setPrefersReduced(e.matches)
    mediaQuery.addEventListener('change', handler)
    return () => mediaQuery.removeEventListener('change', handler)
  }, [])
 
  return prefersReduced
}

User Toggle Control

function MotionPreferences() {
  const [motionEnabled, setMotionEnabled] = useState(true)
 
  useEffect(() => {
    document.body.classList.toggle('reduce-motion', !motionEnabled)
  }, [motionEnabled])
 
  return (
    <label className="motion-toggle">
      <input
        type="checkbox"
        checked={motionEnabled}
        onChange={(e) => setMotionEnabled(e.target.checked)}
      />
      Enable motion effects
    </label>
  )
}
/* Global disable via user preference */
.reduce-motion * {
  animation-duration: 0.01ms !important;
  transition-duration: 0.01ms !important;
  transform: none !important;
}

Safe Static Fallback

/* Default: no parallax */
.hero-section {
  background-image: url('hero.jpg');
  background-size: cover;
  background-position: center;
}
 
/* Parallax only with explicit motion preference */
@media (prefers-reduced-motion: no-preference) {
  .hero-section {
    background-attachment: fixed;
  }
}

Content Must Work Without Effect

<!-- ❌ Bad: Parallax hides essential content -->
<section class="parallax-only">
  <div class="background-layer">Important info here</div>
</section>
 
<!-- ✅ Good: Content accessible regardless of effect -->
<section class="hero">
  <div class="hero-background" aria-hidden="true"></div>
  <div class="hero-content">
    <h1>Welcome</h1>
    <p>All content readable without parallax</p>
  </div>
</section>

Exceptions

  • Evaluate the rendered experience before treating a static-code smell as a blocker; interaction timing, browser behavior, and assistive technology output often determine severity.
  • Not every secondary accessibility issue deserves equal weight; prioritize the issue that most directly blocks perception, operation, or understanding.
  • Avoid adding redundant markup or ARIA solely to satisfy a rule when a simpler semantic implementation would eliminate the issue entirely.

Verification

Automated Checks

  • Use browser accessibility tooling, axe, Lighthouse, or equivalent automated checks against a representative rendered state.

Manual Checks

  • Enable "Reduce motion" in OS accessibility settings
  • Verify parallax effects are disabled
  • Confirm all content is still visible and readable
  • Check that page layout doesn't break without effects
  • Test user toggle if provided

Use with AI

Copy these prompts to use with your AI assistant, or install the MCP server to use directly from Claude, Cursor, or Windsurf.

Check

Verify implementation

Identify parallax effects where background and foreground move at different speeds. Verify these effects are disabled when prefers-reduced-motion is set, and essential content is accessible without the effect.

Fix

Auto-fix issues

Wrap parallax effects in @media (prefers-reduced-motion: no-preference) queries. Provide static fallbacks that convey the same content. Consider adding a user toggle to disable motion effects site-wide.

Explain

Learn more

Explain how parallax effects create a depth mismatch that can trigger vestibular disorders, causing dizziness, nausea, and disorientation in affected users.

Review

Code review

Review the rendered markup and interactive states that affect Provide alternatives to parallax effects. Flag exact elements, roles, labels, focus behavior, or keyboard interactions that violate the rule, and note how to verify the fix with browser accessibility tooling or assistive tech.

Sources

References used to support the guidance in this rule.

Further Reading

Tools and supplementary material for exploring the topic in more depth.

axe DevTools
deque.comTool

Rules that often go hand-in-hand with this one.

Respect reduced motion preferences

Animations respect user motion preferences, avoid seizure-triggering flashing, and include warnings for excessive motion.

Accessibility
Provide instant anchor scroll option

Smooth scroll animations to anchor links respect motion preferences or provide an instant alternative.

Accessibility
Avoid scrolljacking and custom scroll behavior

Natural scroll behavior is preserved without custom scroll speeds, directions, or hijacked scroll events.

Accessibility
Prevent seizure-triggering flashing content

Content does not flash more than three times per second to prevent seizures in users with photosensitive epilepsy.

Accessibility

Was this rule helpful?

Your feedback helps improve rule quality. This stays internal for now.

Loading feedback...
0 / 385