useVirtualFocus
A composable for keyboard navigation where DOM focus stays on a single control element while a visual highlight moves across items.
Usage
useVirtualFocus manages a virtual highlight across a list of items — DOM focus never leaves the control element (typically an <input>). Arrow keys move the highlight, aria-activedescendant on the control references the active item, and data-highlighted is set on the highlighted element. This is the standard pattern for comboboxes, autocompletes, and searchable selects.
Architecture
useVirtualFocus shares its traversal kernel with useRovingFocus — both build on createFocusTraversal. The difference: roving focus moves real DOM focus between items, while virtual focus keeps DOM focus on a control and moves a data attribute highlight.
Options
| Option | Type | Default | Notes |
|---|---|---|---|
control | MaybeRefOrGetter<HTMLElement | null> | — | Required. Element that holds DOM focus and receives aria-activedescendant |
target | MaybeRefOrGetter<HTMLElement | null> | control | Element to attach keydown listener to (defaults to control) |
orientation | 'horizontal' | 'vertical' | 'both' | 'vertical' | Arrow key axis. Ignored when columns is set — grid navigation uses all 4 arrows |
circular | boolean | false | Wrap around when navigating past first/last item |
columns | MaybeRefOrGetter<number> | — | Column count for 2D grid navigation. Left/Right step ±1, Up/Down step ±columns, Home/End jump to row edges, Ctrl+Home/End go to first/last item |
onHighlight | (id: ID) => void | — | Called when the highlighted item changes |
Reactivity
| Property/Method | Reactive | Notes |
|---|---|---|
highlightedId | ShallowRef, tracks the currently highlighted item | |
highlight(id) | - | Programmatically highlight an item by ID |
clear() | - | Remove the highlight |
next() | - | Move highlight to next enabled item |
prev() | - | Move highlight to previous enabled item |
first() | - | Move highlight to first enabled item |
last() | - | Move highlight to last enabled item |
onKeydown | - | Keydown handler — auto-bound to target or control |
Examples
- Apple
- Banana
- Blueberry
- Cherry
- Dragon Fruit
- Grape
- Kiwi
- Lemon
- Mango
- Orange
- Peach
- Pear
- Strawberry
- Watermelon
FAQ
Use useVirtualFocus when a single control (usually an <input>) keeps DOM focus while a highlight moves across non-focusable options — comboboxes, autocompletes, searchable selects. Use useRovingFocus when the items are real focusable elements that should receive browser focus directly.
aria-activedescendant on the control references the active item’s id, and data-highlighted is set on the highlighted element so you can style it with a [data-highlighted] selector.
Yes. Call clear() when the list changes (e.g. watch(filtered, () => clear())) so the highlight never points at a stale or removed item.