0
# Focus Navigation Utilities
1
2
Low-level utilities for creating custom focus managers, traversing focusable elements, and implementing keyboard navigation patterns in complex UI components.
3
4
## Capabilities
5
6
### createFocusManager Function
7
8
Creates a FocusManager object for moving focus within a specific element, independent of FocusScope components.
9
10
```typescript { .api }
11
/**
12
* Creates a FocusManager object that can be used to move focus within an element.
13
*/
14
function createFocusManager(
15
ref: RefObject<Element | null>,
16
defaultOptions?: FocusManagerOptions
17
): FocusManager;
18
19
interface FocusManager {
20
/** Moves focus to the next focusable or tabbable element in the focus scope. */
21
focusNext(opts?: FocusManagerOptions): FocusableElement | null;
22
/** Moves focus to the previous focusable or tabbable element in the focus scope. */
23
focusPrevious(opts?: FocusManagerOptions): FocusableElement | null;
24
/** Moves focus to the first focusable or tabbable element in the focus scope. */
25
focusFirst(opts?: FocusManagerOptions): FocusableElement | null;
26
/** Moves focus to the last focusable or tabbable element in the focus scope. */
27
focusLast(opts?: FocusManagerOptions): FocusableElement | null;
28
}
29
30
interface FocusManagerOptions {
31
/** The element to start searching from. The currently focused element by default. */
32
from?: Element;
33
/** Whether to only include tabbable elements, or all focusable elements. */
34
tabbable?: boolean;
35
/** Whether focus should wrap around when it reaches the end of the scope. */
36
wrap?: boolean;
37
/** A callback that determines whether the given element is focused. */
38
accept?: (node: Element) => boolean;
39
}
40
```
41
42
**Usage Examples:**
43
44
```typescript
45
import React, { useRef, useEffect } from "react";
46
import { createFocusManager } from "@react-aria/focus";
47
48
// Custom grid navigation
49
function NavigableGrid({ items, columns }) {
50
const gridRef = useRef<HTMLDivElement>(null);
51
const focusManager = useRef<FocusManager>();
52
53
useEffect(() => {
54
focusManager.current = createFocusManager(gridRef, {
55
tabbable: true,
56
wrap: false
57
});
58
}, []);
59
60
const handleKeyDown = (e: React.KeyboardEvent) => {
61
const manager = focusManager.current;
62
if (!manager) return;
63
64
switch (e.key) {
65
case 'ArrowRight':
66
e.preventDefault();
67
manager.focusNext();
68
break;
69
case 'ArrowLeft':
70
e.preventDefault();
71
manager.focusPrevious();
72
break;
73
case 'Home':
74
e.preventDefault();
75
manager.focusFirst();
76
break;
77
case 'End':
78
e.preventDefault();
79
manager.focusLast();
80
break;
81
}
82
};
83
84
return (
85
<div
86
ref={gridRef}
87
className="grid"
88
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
89
onKeyDown={handleKeyDown}
90
>
91
{items.map((item, index) => (
92
<button key={index} tabIndex={index === 0 ? 0 : -1}>
93
{item}
94
</button>
95
))}
96
</div>
97
);
98
}
99
100
// Custom focus manager with filtering
101
function FilteredNavigation({ items, isDisabled }) {
102
const containerRef = useRef<HTMLDivElement>(null);
103
const focusManager = useRef<FocusManager>();
104
105
useEffect(() => {
106
focusManager.current = createFocusManager(containerRef);
107
}, []);
108
109
const focusNextEnabled = () => {
110
focusManager.current?.focusNext({
111
accept: (node) => {
112
const index = parseInt(node.getAttribute('data-index') || '0');
113
return !isDisabled(items[index]);
114
}
115
});
116
};
117
118
const focusPreviousEnabled = () => {
119
focusManager.current?.focusPrevious({
120
accept: (node) => {
121
const index = parseInt(node.getAttribute('data-index') || '0');
122
return !isDisabled(items[index]);
123
}
124
});
125
};
126
127
return (
128
<div ref={containerRef}>
129
<button onClick={focusPreviousEnabled}>Previous Enabled</button>
130
<button onClick={focusNextEnabled}>Next Enabled</button>
131
{items.map((item, index) => (
132
<button
133
key={index}
134
data-index={index}
135
disabled={isDisabled(item)}
136
tabIndex={-1}
137
>
138
{item.name}
139
</button>
140
))}
141
</div>
142
);
143
}
144
```
145
146
### getFocusableTreeWalker Function
147
148
Creates a TreeWalker that matches all focusable or tabbable elements within a root element, with optional filtering.
149
150
```typescript { .api }
151
/**
152
* Create a TreeWalker that matches all focusable/tabbable elements.
153
*/
154
function getFocusableTreeWalker(
155
root: Element,
156
opts?: FocusManagerOptions,
157
scope?: Element[]
158
): TreeWalker | ShadowTreeWalker;
159
```
160
161
**Usage Examples:**
162
163
```typescript
164
import React, { useRef } from "react";
165
import { getFocusableTreeWalker } from "@react-aria/focus";
166
167
// Find all focusable elements
168
function FocusableElementFinder() {
169
const containerRef = useRef<HTMLDivElement>(null);
170
171
const findFocusableElements = () => {
172
if (!containerRef.current) return [];
173
174
const walker = getFocusableTreeWalker(containerRef.current, {
175
tabbable: false // Include all focusable elements, not just tabbable ones
176
});
177
178
const elements: Element[] = [];
179
let node = walker.nextNode() as Element;
180
181
while (node) {
182
elements.push(node);
183
node = walker.nextNode() as Element;
184
}
185
186
return elements;
187
};
188
189
const logFocusableElements = () => {
190
const elements = findFocusableElements();
191
console.log('Focusable elements:', elements);
192
};
193
194
return (
195
<div ref={containerRef}>
196
<button onClick={logFocusableElements}>Find Focusable Elements</button>
197
<input type="text" placeholder="Focusable input" />
198
<button>Focusable button</button>
199
<div tabIndex={0}>Focusable div</div>
200
<a href="#" tabIndex={-1}>Non-tabbable link</a>
201
<button disabled>Disabled button</button>
202
</div>
203
);
204
}
205
206
// Count tabbable elements
207
function TabbableCounter() {
208
const containerRef = useRef<HTMLFormElement>(null);
209
210
const countTabbableElements = () => {
211
if (!containerRef.current) return 0;
212
213
const walker = getFocusableTreeWalker(containerRef.current, {
214
tabbable: true
215
});
216
217
let count = 0;
218
while (walker.nextNode()) {
219
count++;
220
}
221
222
return count;
223
};
224
225
return (
226
<form ref={containerRef}>
227
<p>Tabbable elements: {countTabbableElements()}</p>
228
<input type="text" />
229
<button type="button">Button</button>
230
<select>
231
<option>Option 1</option>
232
</select>
233
<textarea></textarea>
234
</form>
235
);
236
}
237
238
// Custom traversal with filtering
239
function FilteredTraversal() {
240
const containerRef = useRef<HTMLDivElement>(null);
241
242
const findButtonElements = () => {
243
if (!containerRef.current) return [];
244
245
const walker = getFocusableTreeWalker(containerRef.current, {
246
tabbable: true,
247
accept: (node) => node.tagName === 'BUTTON'
248
});
249
250
const buttons: Element[] = [];
251
let node = walker.nextNode() as Element;
252
253
while (node) {
254
buttons.push(node);
255
node = walker.nextNode() as Element;
256
}
257
258
return buttons;
259
};
260
261
return (
262
<div ref={containerRef}>
263
<input type="text" />
264
<button>Button 1</button>
265
<select><option>Select</option></select>
266
<button>Button 2</button>
267
<textarea></textarea>
268
<button>Button 3</button>
269
<p>Found {findButtonElements().length} buttons</p>
270
</div>
271
);
272
}
273
```
274
275
### useHasTabbableChild Hook
276
277
Returns whether an element has a tabbable child and updates as children change.
278
279
```typescript { .api }
280
/**
281
* Returns whether an element has a tabbable child, and updates as children change.
282
* @private - Internal utility for special cases
283
*/
284
function useHasTabbableChild(
285
ref: RefObject<Element | null>,
286
options?: AriaHasTabbableChildOptions
287
): boolean;
288
289
interface AriaHasTabbableChildOptions {
290
isDisabled?: boolean;
291
}
292
```
293
294
**Usage Examples:**
295
296
```typescript
297
import React, { useRef } from "react";
298
import { useHasTabbableChild } from "@react-aria/focus";
299
300
// Dynamic empty state handling
301
function CollectionContainer({ items, emptyMessage }) {
302
const containerRef = useRef<HTMLDivElement>(null);
303
const hasTabbableChild = useHasTabbableChild(containerRef);
304
305
// Show different tabIndex based on whether container has tabbable children
306
const containerTabIndex = hasTabbableChild ? -1 : 0;
307
308
return (
309
<div
310
ref={containerRef}
311
tabIndex={containerTabIndex}
312
role="grid"
313
aria-label={items.length === 0 ? "Empty collection" : "Collection"}
314
>
315
{items.length > 0 ? (
316
items.map((item, index) => (
317
<button key={index} role="gridcell">
318
{item.name}
319
</button>
320
))
321
) : (
322
<div role="gridcell">
323
{emptyMessage}
324
<button>Add Item</button>
325
</div>
326
)}
327
</div>
328
);
329
}
330
331
// Conditional keyboard navigation
332
function ConditionalNavigation({ isNavigationDisabled, children }) {
333
const containerRef = useRef<HTMLDivElement>(null);
334
const hasTabbableChild = useHasTabbableChild(containerRef, {
335
isDisabled: isNavigationDisabled
336
});
337
338
return (
339
<div
340
ref={containerRef}
341
data-has-tabbable-child={hasTabbableChild}
342
tabIndex={hasTabbableChild ? -1 : 0}
343
>
344
Navigation Status: {hasTabbableChild ? 'Has tabbable children' : 'No tabbable children'}
345
{children}
346
</div>
347
);
348
}
349
```
350
351
## Focus Navigation Patterns
352
353
### Tree Walker Behavior
354
355
The `getFocusableTreeWalker` function creates a DOM TreeWalker that:
356
- Traverses elements in document order
357
- Filters based on focusability rules (CSS, disabled state, visibility)
358
- Supports both focusable and tabbable element detection
359
- Handles Shadow DOM traversal when available
360
- Respects custom acceptance criteria
361
362
### Focusable vs Tabbable
363
364
**Focusable Elements:**
365
- Can receive focus via JavaScript (`element.focus()`)
366
- Includes elements with `tabIndex="-1"`
367
- May not be reachable via Tab navigation
368
369
**Tabbable Elements:**
370
- Subset of focusable elements
371
- Reachable via Tab/Shift+Tab navigation
372
- Have `tabIndex >= 0` or are naturally tabbable (buttons, inputs, etc.)
373
374
### Custom Navigation Patterns
375
376
Common patterns supported by these utilities:
377
378
- **Arrow Key Navigation**: Grid, list, and menu navigation
379
- **Page Up/Down**: Large list scrolling with focus management
380
- **Home/End Keys**: Jump to first/last focusable element
381
- **Letter Navigation**: Type-ahead search in lists
382
- **Roving TabIndex**: Single tab stop with internal arrow key navigation
383
384
### Performance Considerations
385
386
- TreeWalkers are more efficient than `querySelectorAll` for large DOMs
387
- Focus managers cache the root element reference
388
- MutationObserver in `useHasTabbableChild` automatically updates on DOM changes
389
- Use `tabbable: true` when possible to reduce the search space