Skip to main content

Keyboard Navigation

The Virtuoso component exposes an imperative scrollIntoView method, which makes it easy to implement keyboard navigation. As an optional configuration, the method accepts behavior: 'smooth' | 'auto', and a done callback which gets called after the scrolling is done. See the example below for its usage.

To test the example below, click anywhere in the list and press up / down arrows.

Live Editor
function App() {
  const ref = React.useRef(null)
  const [currentItemIndex, setCurrentItemIndex] = React.useState(-1)
  const listRef = React.useRef(null)

  const keyDownCallback = React.useCallback(
    (e) => {
      let nextIndex = null

      if (e.code === 'ArrowUp') {
        nextIndex = Math.max(0, currentItemIndex - 1)
      } else if (e.code === 'ArrowDown') {
        nextIndex = Math.min(99, currentItemIndex + 1)
      }

      if (nextIndex !== null) {
        ref.current.scrollIntoView({
          index: nextIndex,
          behavior: 'auto',
          done: () => {
            setCurrentItemIndex(nextIndex)
          },
        })
        e.preventDefault()
      }
    },
    [currentItemIndex, ref, setCurrentItemIndex]
  )

  const scrollerRef = React.useCallback(
    (element) => {
      if (element) {
        element.addEventListener('keydown', keyDownCallback)
        listRef.current = element
      } else {
        listRef.current.removeEventListener('keydown', keyDownCallback)
      }
    },
    [keyDownCallback]
  )

  return (
    <Virtuoso
      ref={ref}
      totalCount={100}
      context={{ currentItemIndex }}
      itemContent={(index, _, { currentItemIndex }) => (
        <div
          style={{
            borderColor: index === currentItemIndex ? 'blue' : 'transparent',
            borderSize: '1px',
            borderStyle: 'solid',
            padding: '0.5rem 0.2rem',
          }}
        >
          <div style={{ marginTop: '1rem' }}>Item {index + 1}</div>
        </div>
      )}
      scrollerRef={scrollerRef}
      style={{ height: 400 }}
    />
  )
}
Result
Loading...