Skip to content

useListNavigation

Adds arrow key-based navigation for a list/grid of items inside a floating element, with optional virtual focus (aria-activedescendant), RTL, nested close behavior, and looping.

Signature

ts
function useListNavigation(
  context: FloatingContext | TreeNode<FloatingContext>,
  options: UseListNavigationOptions
): UseListNavigationReturn

Parameters

ParameterTypeRequiredDescription
contextFloatingContext | TreeNode<FloatingContext>YesContext from useFloating or a tree node for nested menus
optionsUseListNavigationOptionsYesConfiguration options (see below)

Options

ts
interface UseListNavigationOptions {
  listRef: Ref<Array<HTMLElement | null>>
  activeIndex?: MaybeRefOrGetter<number | null>
  onNavigate?: (index: number | null) => void
  enabled?: MaybeRefOrGetter<boolean>
  loop?: MaybeRefOrGetter<boolean>
  orientation?: MaybeRefOrGetter<'vertical' | 'horizontal' | 'both'>
  disabledIndices?: Array<number> | ((index: number) => boolean)
  focusItemOnHover?: MaybeRefOrGetter<boolean>
  openOnArrowKeyDown?: MaybeRefOrGetter<boolean>
  scrollItemIntoView?: boolean | ScrollIntoViewOptions
  selectedIndex?: MaybeRefOrGetter<number | null>
  focusItemOnOpen?: MaybeRefOrGetter<boolean | 'auto'>
  nested?: MaybeRefOrGetter<boolean>
  parentOrientation?: MaybeRefOrGetter<'vertical' | 'horizontal' | 'both'>
  rtl?: MaybeRefOrGetter<boolean>
  virtual?: MaybeRefOrGetter<boolean>
  virtualItemRef?: Ref<HTMLElement | null>
  cols?: MaybeRefOrGetter<number>
  allowEscape?: MaybeRefOrGetter<boolean>
  gridLoopDirection?: MaybeRefOrGetter<'row' | 'next'>
}
OptionTypeDefaultDescription
listRef`Ref<HTMLElement[](HTMLElement|null)[]>`
activeIndexMaybeRefOrGetter<number|null>nullCurrent active/highlighted item index.
onNavigate(index: number|null) => voidCalled on navigation changes. You should update activeIndex.
enabledMaybeRefOrGetter<boolean>trueEnable/disable all listeners.
loopMaybeRefOrGetter<boolean>falseWrap at boundaries. With virtual + allowEscape it can deselect.
orientation`MaybeRefOrGetter<'vertical''horizontal''both'>`
disabledIndicesnumber[] | (index:number)=>booleanundefinedSkip these indices when navigating.
focusItemOnHoverMaybeRefOrGetter<boolean>trueHovering an item sets it active.
openOnArrowKeyDownMaybeRefOrGetter<boolean>trueArrow on anchor opens floating and activates an item.
scrollItemIntoViewboolean | ScrollIntoViewOptionstrueScroll active item into view (nearest by default); suppressed during pointer modality.
selectedIndexMaybeRefOrGetter<number|null>nullPreferred item to activate on open if provided.
focusItemOnOpen`MaybeRefOrGetter<boolean'auto'>`'auto'
nestedMaybeRefOrGetter<boolean>falseEnables cross-axis close to return focus to parent anchor.
parentOrientation`MaybeRefOrGetter<'vertical''horizontal''both'>`
rtlMaybeRefOrGetter<boolean>falseFlips Left/Right behavior for horizontal/both orientations.
virtualMaybeRefOrGetter<boolean>falseKeep DOM focus on anchor and manage active with aria-activedescendant.
virtualItemRef`Ref<HTMLElementnull>`
colsMaybeRefOrGetter<number>1Grid columns when > 1; ArrowUp/Down move by ±cols (uniform grid).
allowEscapeMaybeRefOrGetter<boolean>falseWith virtual and loop, navigating past ends yields onNavigate(null).
gridLoopDirectionMaybeRefOrGetter<'row'|'next'>'row'Grid horizontal wrapping behavior. 'row' wraps within same row, 'next' moves to next/prev row.

Return Value

ts
interface UseListNavigationReturn {
  cleanup: () => void
}
PropertyTypeDescription
cleanup() => voidRemoves internal event listeners.

Examples

Basic Menu Navigation

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useFloating, useListNavigation } from 'v-float'

const anchorEl = ref<HTMLElement|null>(null)
const floatingEl = ref<HTMLElement|null>(null)
const itemsRef = ref<Array<HTMLElement|null>>([])
const activeIndex = ref<number|null>(null)

const ctx = useFloating(anchorEl, floatingEl)

useListNavigation(ctx, {
  listRef: itemsRef,
  activeIndex,
  onNavigate: (i) => (activeIndex.value = i),
  orientation: 'vertical',
  loop: true,
})
</script>

<template>
  <button ref="anchorEl">Open</button>
  <ul v-if="ctx.open.value" ref="floatingEl">
    <li v-for="(opt, i) in 5" :key="i" :ref="el => itemsRef.value[i] = el" tabindex="-1">
      Item {{ i + 1 }}
    </li>
  </ul>
</template>

Virtual Listbox (aria-activedescendant)

vue
<script setup lang="ts">
import { ref } from 'vue'
import { useFloating, useListNavigation } from 'v-float'

const anchorEl = ref<HTMLElement|null>(null)
const floatingEl = ref<HTMLElement|null>(null)
const itemsRef = ref<Array<HTMLElement|null>>([])
const activeIndex = ref<number|null>(null)
const virtItem = ref<HTMLElement|null>(null)

const ctx = useFloating(anchorEl, floatingEl)

useListNavigation(ctx, {
  listRef: itemsRef,
  activeIndex,
  onNavigate: (i) => (activeIndex.value = i),
  virtual: true,
  virtualItemRef: virtItem,
  openOnArrowKeyDown: true,
})
</script>

<template>
  <input ref="anchorEl" role="combobox" :aria-expanded="ctx.open.value" />
  <ul v-if="ctx.open.value" ref="floatingEl" role="listbox">
    <li v-for="(opt, i) in 5" :key="i" :ref="el => itemsRef.value[i] = el" role="option">
      {{ i }}
    </li>
  </ul>
</template>

Nested Submenu (cross-axis close)

ts
import { useFloatingTree, useListNavigation } from 'v-float'
// ... create tree, add nodes
useListNavigation(childNode, { nested: true, orientation: 'horizontal' })

Uniform Grid Navigation

ts
useListNavigation(ctx, {
  listRef: itemsRef,
  activeIndex,
  onNavigate: i => activeIndex.value = i,
  cols: 4,
  orientation: 'both',
  gridLoopDirection: 'next' // Optional
})

See Also

Released under the MIT License.