Skip to content

Keyboard Navigation

useVirtualKeyboardNav() — attaches keydown listeners and manages a focused index. Works with any virtual list component.

ts
function useVirtualKeyboardNav(options: UseVirtualKeyboardNavOptions): UseVirtualKeyboardNavReturn

Options

itemCount

Ref<number> | number

Total item count.

scrollTo

(index, align?) => void

Called to scroll the list when focus moves.

target

Ref<HTMLElement | null> | HTMLElement · default: document

Element that receives keyboard events.

onActivate

(index: number) => void

Called on Enter or Space.

onChange

(index: number) => void

Called when the focused index changes.

loop

boolean · default: false

Whether to wrap around at boundaries.

Return value

focusedIndex

Readonly<Ref<number>>

Currently focused index, -1 if nothing focused.

setFocus

(index: number) => void

Programmatically set focus.

isFocused

(index: number) => boolean

Whether index is focused.

Keys handled

KeyAction
Move focus up
Move focus down
HomeFocus first item
EndFocus last item
PageUpJump −10 items
PageDownJump +10 items
Enter / SpaceCall onActivate

Example

vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { VirtualList, useVirtualKeyboardNav } from 'vue-virtual-scroller-kit'
import type { VirtualListExpose } from 'vue-virtual-scroller-kit'

const items = ref(Array.from({ length: 1000 }, (_, i) => ({ id: i, label: `Item ${i + 1}` })))
const listRef = ref<VirtualListExpose | null>(null)

const { focusedIndex, setFocus, isFocused } = useVirtualKeyboardNav({
  itemCount: computed(() => items.value.length),
  scrollTo: (i, align) => listRef.value?.scrollTo(i, align),
  onActivate: (i) => console.log('Activated', items.value[i]),
  loop: false,
})
</script>

<template>
  <div tabindex="0" style="outline: none; border: 1px solid #ccc">
    <VirtualList ref="listRef" :items="items" :estimated-item-size="48" style="height: 400px">
      <template #default="{ item, index }">
        <div
          :class="{ focused: isFocused(index) }"
          :aria-selected="isFocused(index)"
          role="option"
          @click="setFocus(index)"
        >
          {{ item.label }}
        </div>
      </template>
    </VirtualList>
  </div>
</template>