Skip to content

useRovingFocus

useRovingFocus implements the WAI-ARIA roving tabindex pattern. It manages physical DOM focus across an item list, assigning tabindex="0" to the active item and tabindex="-1" to all others. Arrow keys, Home, End, and PageUp/PageDown shift DOM focus directly to target items.

Implements the NavigationTarget protocol for seamless integration with auxiliary composables like useTypeahead.

Type

ts
function useRovingFocus(
  node: FloatingNode,
  options: UseRovingFocusOptions,
): UseRovingFocusReturn;

type RovingEntryFocusMode = "entry-index" | "last-focused";

interface UseRovingFocusOptions {
  elementsList: MaybeRefOrGetter<Array<HTMLElement | null>>;
  containerEl?: MaybeRefOrGetter<HTMLElement | null>;
  activeIndex?: Ref<number>;
  entryIndex?: MaybeRefOrGetter<number | null | undefined>;
  entryFocusMode?: MaybeRefOrGetter<RovingEntryFocusMode>;
  orientation?: MaybeRefOrGetter<"vertical" | "horizontal" | "both">;
  loop?: MaybeRefOrGetter<boolean>;
  pageSize?: MaybeRefOrGetter<number>;
  rtl?: MaybeRefOrGetter<boolean>;
  enabled?: MaybeRefOrGetter<boolean>;
  scrollIntoView?: MaybeRefOrGetter<boolean>;
  focusOnHover?: MaybeRefOrGetter<boolean>;
  focusDisabledElements?: MaybeRefOrGetter<boolean>;
  isItemDisabled?: (index: number) => boolean;
  onSelect?: (index: number, event: Event) => void;
  onActiveIndexChange?: (index: number) => void;
  onEnter?: (index: number, event: KeyboardEvent) => void;
  onExit?: (index: number, event: KeyboardEvent) => void;
  isKeyHandled?: (event: KeyboardEvent) => boolean;
}

interface UseRovingFocusReturn extends NavigationTarget {
  readonly activeIndex: Readonly<Ref<number>>;
  tabStopIndex: ComputedRef<number>;
  setActiveIndex: (index: number) => void;
  clearActive: () => void;
  reset: () => void;
  focusIndex: (target: NavigationTargetValue, options?: NavigationTargetOptions) => void;
  getTabindex: (index: number) => 0 | -1;
}

Options

NameTypeDefaultNotes
elementsListMaybeRefOrGetter<Array<HTMLElement | null>>RequiredThe list of HTML element references representing navigable elements.
containerElMaybeRefOrGetter<HTMLElement | null>node.refs.floatingElContainer receiving keyboard and pointer events and used for RTL detection.
activeIndexRef<number>undefinedControlled active index ref.
entryIndexMaybeRefOrGetter<number | null | undefined>0Default entry item holding tabindex="0" when idle. Set -1 for menus.
entryFocusModeMaybeRefOrGetter<RovingEntryFocusMode>"entry-index"Whether re-entry restores "entry-index" or "last-focused".
orientationMaybeRefOrGetter<"vertical" | "horizontal" | "both">"vertical"Direction of navigation.
loopMaybeRefOrGetter<boolean>falseWhen true, navigation wraps around at boundaries.
pageSizeMaybeRefOrGetter<number>10Number of items jumped on PageUp and PageDown.
rtlMaybeRefOrGetter<boolean>Auto-detectedWhether layout follows Right-to-Left reading order.
enabledMaybeRefOrGetter<boolean>trueWhen false, keyboard listeners are inactive.
scrollIntoViewMaybeRefOrGetter<boolean>trueWhether focused elements are automatically scrolled into view.
focusOnHoverMaybeRefOrGetter<boolean>falseWhen true, moving the pointer over an item focuses it.
focusDisabledElementsMaybeRefOrGetter<boolean>falseAllows disabled items to receive focus for APG discoverability.
isItemDisabled(index: number) => booleanAuto-detectedCustom predicate for disabled state.
onSelect(index: number, event: Event) => voidundefinedCallback fired on Enter or Space.
onActiveIndexChange(index: number) => voidundefinedCallback fired when active item index changes.
onEnter(index: number, event: KeyboardEvent) => voidundefinedCallback fired on ArrowRight (LTR) / ArrowLeft (RTL) for submenu opening.
onExit(index: number, event: KeyboardEvent) => voidundefinedCallback fired on ArrowLeft (LTR) / ArrowRight (RTL) for submenu closing.

Returns

NameTypeNotes
activeIndexReadonly<Ref<number>>Index of the currently focused item (-1 when unfocused / idle).
tabStopIndexComputedRef<number>Index of the element that currently holds tabindex="0".
focusIndex(target: NavigationTargetValue, options?: NavigationTargetOptions) => voidPolymorphic navigation method. Accepts an index, "reset", or directional keywords ("next", "prev", "first", "last", "page-up", "page-down").
setActiveIndex(index: number) => voidSets active index without focusing the DOM element.
clearActive() => voidClears active focus state.
reset() => voidResets activeIndex and focus history back to initial conditions.
getTabindex(index: number) => 0 | -1Returns 0 if index === tabStopIndex, otherwise -1. Bind to :tabindex.

Example

vue
<script setup lang="ts">
import { shallowRef } from "vue";
import { useFloatingNode, useRovingFocus } from "v-float";

const items = ["Profile", "Account Settings", "Billing", "Logout"];
const anchorEl = shallowRef<HTMLElement | null>(null);
const floatingEl = shallowRef<HTMLElement | null>(null);
const elementsList = shallowRef<Array<HTMLElement | null>>([]);

const node = useFloatingNode({ anchorEl, floatingEl });

const { activeIndex, getTabindex, focusIndex } = useRovingFocus(node, {
  elementsList,
  orientation: "vertical",
  loop: true,
});
</script>

<template>
  <button ref="anchorEl" type="button" @click="node.open.value = !node.open.value">
    Options
  </button>

  <div
    v-if="node.open.value"
    ref="floatingEl"
    role="menu"
    class="menu"
  >
    <button
      v-for="(item, idx) in items"
      :key="item"
      :ref="(el) => (elementsList[idx] = el as HTMLElement | null)"
      role="menuitem"
      :tabindex="getTabindex(idx)"
      :class="{ focused: activeIndex === idx }"
      @click="focusIndex(idx)"
    >
      {{ item }}
    </button>
  </div>
</template>

See Also