Skip to main content
CodeRocket
Accessibilitylowanimation

Provide instant anchor scroll option

rule · anchor-smooth-scroll

Smooth scrolling should respect motion preferences to avoid triggering vestibular disorders.

Code Example

CSS
/* ❌ Bad: Always smooth, ignores preferences */
html {
  scroll-behavior: smooth;
}
 
/* ✅ Good: Respect motion preferences */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}
 
/* Users who prefer reduced motion get instant scrolling (default) */

Why It Matters

Smooth scroll animations can trigger vertigo, nausea, and disorientation for users with vestibular disorders—instant navigation is safer and often faster.

JavaScript Approach

TypeScript
function scrollToElement(elementId: string) {
  const element = document.getElementById(elementId)
  if (!element) return
 
  const prefersReducedMotion = window.matchMedia(
    '(prefers-reduced-motion: reduce)'
  ).matches
 
  element.scrollIntoView({
    behavior: prefersReducedMotion ? 'instant' : 'smooth',
    block: 'start'
  })
 
  // Move focus to target for keyboard users
  element.focus({ preventScroll: true })
}

React Hook

TSX
function useScrollToAnchor() {
  const prefersReducedMotion = useReducedMotion()
 
  const scrollTo = useCallback((elementId: string) => {
    const element = document.getElementById(elementId)
    if (!element) return
 
    element.scrollIntoView({
      behavior: prefersReducedMotion ? 'instant' : 'smooth',
      block: 'start'
    })
 
    // Set tabindex if element isn't focusable
    if (!element.hasAttribute('tabindex')) {
      element.setAttribute('tabindex', '-1')
    }
    element.focus({ preventScroll: true })
  }, [prefersReducedMotion])
 
  return scrollTo
}
 
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
}
TSX
interface AnchorLinkProps {
  href: string
  children: React.ReactNode
}
 
function AnchorLink({ href, children }: AnchorLinkProps) {
  const scrollTo = useScrollToAnchor()
 
  const handleClick = (e: React.MouseEvent) => {
    if (href.startsWith('#')) {
      e.preventDefault()
      scrollTo(href.slice(1))
    }
  }
 
  return (
    <a href={href} onClick={handleClick}>
      {children}
    </a>
  )
}

Focus Management

HTML
<!-- Target must be focusable for keyboard navigation -->
<section id="features" tabindex="-1">
  <h2>Features</h2>
  <!-- Content -->
</section>
CSS
/* Hide focus ring when section is focused via anchor */
section:focus {
  outline: none;
}
 
/* But show focus for keyboard navigation */
section:focus-visible {
  outline: 2px solid var(--focus-color);
}

Exceptions

  • Simple data tables can sometimes fail more from missing header relationships than from missing enhancements such as captions or mobile wrappers, so prioritize the strongest semantic issue.
  • Do not convert layout structures into data-table markup just to satisfy a rule; the correct fix may be to remove table semantics entirely.
  • When several table-accessibility issues overlap, resolve the header-cell relationship first because downstream announcements depend on it.

Verification

Automated Checks

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

Manual Checks

  • Enable "Reduce motion" in system accessibility settings
  • Click anchor links—should scroll instantly, not smoothly
  • Verify focus moves to target section
  • Tab through page after anchor click—focus should continue from anchor target