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

Avoid scrolljacking and custom scroll behavior

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

Utilities
Quick take
Typical fix time 15 min
  • Never override native scroll speed or direction
  • Avoid scroll-triggered navigation (one page per scroll)
  • Scrolljacking breaks assistive technologies and user expectations
  • If custom scrolling is essential, provide a way to disable it
Why it matters: Scrolljacking breaks user expectations, interferes with assistive technologies, and creates unpredictable experiences that frustrate users with motor impairments or cognitive disabilities.

Rule Details

Scrolljacking breaks user trust by making the page behave unpredictably.

Code Example

// ❌ Bad: Hijacking scroll for section navigation
window.addEventListener('wheel', (e) => {
  e.preventDefault() // Blocks native scroll
  const direction = e.deltaY > 0 ? 'down' : 'up'
  scrollToNextSection(direction)
})
 
// ❌ Bad: Modifying scroll speed
window.addEventListener('scroll', (e) => {
  window.scrollTo(0, window.scrollY * 0.5) // Half-speed scroll
})
 
// ❌ Bad: Horizontal scroll from vertical input
container.addEventListener('wheel', (e) => {
  e.preventDefault()
  container.scrollLeft += e.deltaY // Confusing!
})

Why It Matters

Scrolljacking breaks user expectations, interferes with assistive technologies, and creates unpredictable experiences that frustrate users with motor impairments or cognitive disabilities.

What Is Scrolljacking?

TypeProblem
Modified scroll speedScroll wheel moves more/less than expected
Scroll direction changeHorizontal scroll on vertical input
Snap-to-sectionEach scroll jump to next "page"
Scroll-triggered animationsAnimation blocks continued scrolling
Infinite scroll without fallbackNo way to reach footer content

Acceptable Scroll Behaviors

/* ✅ OK: CSS scroll snap (user stays in control) */
.container {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;
}
 
.section {
  scroll-snap-align: start;
}
 
/* User can still scroll freely, snap is just a guide */
// ✅ OK: Scroll-triggered animations that don't block
const observer = new IntersectionObserver((entries) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      entry.target.classList.add('animate-in')
    }
  })
}, { threshold: 0.1 })
 
// Animation happens, but scrolling continues normally
document.querySelectorAll('.animate-on-scroll').forEach(el => {
  observer.observe(el)
})

Detecting Scrolljacking

// Console test: check if scrolling is hijacked
function detectScrolljacking() {
  let scrollEvents = 0
  let scrollBlocked = false
 
  const handler = (e) => {
    scrollEvents++
    if (e.defaultPrevented) {
      scrollBlocked = true
      console.warn('Scroll event was prevented!')
    }
  }
 
  window.addEventListener('wheel', handler, { passive: false })
 
  setTimeout(() => {
    window.removeEventListener('wheel', handler)
    console.log(`Scroll events: ${scrollEvents}, Blocked: ${scrollBlocked}`)
  }, 5000)
}
 
detectScrolljacking()

If Custom Scroll Is Required

function ScrollEffects({ children }: { children: React.ReactNode }) {
  const [effectsEnabled, setEffectsEnabled] = useState(true)
  const prefersReducedMotion = useReducedMotion()
 
  // Disable by default if user prefers reduced motion
  useEffect(() => {
    if (prefersReducedMotion) {
      setEffectsEnabled(false)
    }
  }, [prefersReducedMotion])
 
  return (
    <div className={effectsEnabled ? 'scroll-effects-on' : ''}>
      <div className="scroll-toggle" role="region" aria-label="Scroll preferences">
        <label>
          <input
            type="checkbox"
            checked={effectsEnabled}
            onChange={(e) => setEffectsEnabled(e.target.checked)}
          />
          Enable scroll animations
        </label>
      </div>
      {children}
    </div>
  )
}

Infinite Scroll Accessibility

// ❌ Bad: No way to reach footer
function InfiniteList() {
  return (
    <div onScroll={loadMore}>
      {items.map(item => <Item key={item.id} {...item} />)}
      {/* Footer is unreachable! */}
    </div>
  )
}
 
// ✅ Good: Pagination fallback
function AccessibleInfiniteList() {
  return (
    <div>
      {items.map(item => <Item key={item.id} {...item} />)}
 
      <button onClick={loadMore} aria-label="Load more items">
        Load more
      </button>
 
      <nav aria-label="Pagination">
        <a href="?page=1">Page 1</a>
        <a href="?page=2">Page 2</a>
        {/* Footer always reachable via pagination */}
      </nav>
 
      <footer>Contact info, links, etc.</footer>
    </div>
  )
}

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

  • Scroll with mouse wheel—movement should feel natural
  • Use keyboard Page Up/Down—should move predictable amounts
  • Test with trackpad, touchscreen, and scroll wheel
  • Verify assistive technology scroll commands work
  • Confirm all page content (including footer) is reachable

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

Test scrolling behavior and verify it feels native. Check that scroll speed is not modified, scroll direction is not inverted, and scroll events are not hijacked for animations or navigation without user consent.

Fix

Auto-fix issues

Remove JavaScript that overrides default scroll behavior. If custom scrolling is essential, respect prefers-reduced-motion and provide a way to disable custom scroll effects.

Explain

Learn more

Explain how scrolljacking disrupts expected navigation patterns, interferes with assistive technologies, and creates unpredictable experiences for users with motor impairments or cognitive disabilities.

Review

Code review

Review the rendered markup and interactive states that affect Avoid scrolljacking and custom scroll behavior. 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.

Provide alternatives to parallax effects

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

Accessibility
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
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