useRovingFocus
A composable for keyboard navigation within composite widgets using the roving tabindex pattern.
Usage
useRovingFocus manages focus across a group of items — only the active item has tabindex="0", all others have tabindex="-1". Arrow keys move focus between items, automatically skipping disabled ones. Supports linear (horizontal/vertical) and grid (2D) navigation modes.
<script setup lang="ts">
import { useRovingFocus } from '@vuetify/v0'
import { useTemplateRef } from 'vue'
const toolbar = useTemplateRef('toolbar')
const items = [
{ id: 'bold', label: 'Bold' },
{ id: 'italic', label: 'Italic' },
{ id: 'underline', label: 'Underline', disabled: true },
{ id: 'strike', label: 'Strikethrough' },
]
const { focusedId, isTabbable } = useRovingFocus(
() => items.map(item => ({
id: item.id,
el: () => toolbar.value?.querySelector(`[data-id="${item.id}"]`),
disabled: item.disabled,
})),
{ target: toolbar, orientation: 'horizontal' },
)
</script>
<template>
<div ref="toolbar" role="toolbar" aria-label="Formatting">
<button
v-for="item in items"
:key="item.id"
:data-id="item.id"
:tabindex="isTabbable(item.id) ? 0 : -1"
:disabled="item.disabled"
>
{{ item.label }}
</button>
</div>
</template>Architecture
useRovingFocus builds on useEventListener for keydown handling. It is a standalone composable — not part of the registry/selection hierarchy — making it composable alongside createSingle or createSelection for widgets that separate focus from selection (e.g., listboxes, selects).
Reactivity
| Property/Method | Reactive | Notes |
|---|---|---|
focusedId | ShallowRef, tracks currently focused item | |
isTabbable(id) | - | Returns true for the one item that should have tabindex="0" |
focus(id) | - | Programmatically focus an item by ID |
next() | - | Move focus to next enabled item |
prev() | - | Move focus to previous enabled item |
first() | - | Move focus to first enabled item |
last() | - | Move focus to last enabled item |
onKeydown | - | Keydown handler — auto-bound when target is provided |
Examples
Color Grid
2D grid navigation with the columns option. Arrow keys navigate in two dimensions, Home/End are row-scoped, Ctrl+Home/End jump to absolute first/last.
| File | Role |
|---|---|
Grid.vue | Color swatch grid with 2D keyboard navigation |
grid.vue | Entry point rendering a material color palette |
Functions
useRovingFocus
(items: () => RovingItem[], options?: RovingFocusOptions) => RovingFocusReturnOptions
target
MaybeRefOrGetter<HTMLElement | null | undefined>orientation
"horizontal" | "vertical" | "both" | undefinedArrow key mapping. Ignored when `columns` is set (grid uses all 4 arrows).
circular
boolean | undefinedcolumns
MaybeRefOrGetter<number> | undefinedColumn count for grid navigation. When set, items are treated as a 2D grid in row-major order: Left/Right step ±1, Up/Down step ±columns, Home/End go to row start/end, Ctrl+Home/End go to first/last overall.
onFocus
((id: ID) => void) | undefined