0
# Focus & Accessibility
1
2
Focus management, element accessibility checks, and ARIA labeling utilities for building accessible React components.
3
4
## Capabilities
5
6
### Focus Management
7
8
Utilities for managing focus behavior and determining focusability.
9
10
```typescript { .api }
11
/**
12
* Focuses element without scrolling the page
13
* Polyfill for {preventScroll: true} option in older browsers
14
* @param element - Element to focus
15
*/
16
function focusWithoutScrolling(element: FocusableElement): void;
17
18
/**
19
* Determines if element can receive focus
20
* @param element - Element to check
21
* @returns true if element is focusable
22
*/
23
function isFocusable(element: Element): boolean;
24
25
/**
26
* Determines if element is reachable via Tab key
27
* @param element - Element to check
28
* @returns true if element is tabbable (excludes tabindex="-1")
29
*/
30
function isTabbable(element: Element): boolean;
31
32
type FocusableElement = HTMLElement | SVGElement;
33
```
34
35
**Usage Examples:**
36
37
```typescript
38
import { focusWithoutScrolling, isFocusable, isTabbable } from "@react-aria/utils";
39
40
function FocusManager({ children }) {
41
const containerRef = useRef<HTMLDivElement>(null);
42
43
const focusFirstElement = () => {
44
if (!containerRef.current) return;
45
46
// Find first focusable element
47
const focusableElements = Array.from(
48
containerRef.current.querySelectorAll('*')
49
).filter(isFocusable);
50
51
if (focusableElements.length > 0) {
52
focusWithoutScrolling(focusableElements[0] as FocusableElement);
53
}
54
};
55
56
const focusFirstTabbable = () => {
57
if (!containerRef.current) return;
58
59
// Find first tabbable element (keyboard accessible)
60
const tabbableElements = Array.from(
61
containerRef.current.querySelectorAll('*')
62
).filter(isTabbable);
63
64
if (tabbableElements.length > 0) {
65
focusWithoutScrolling(tabbableElements[0] as FocusableElement);
66
}
67
};
68
69
return (
70
<div ref={containerRef}>
71
<button onClick={focusFirstElement}>Focus First Focusable</button>
72
<button onClick={focusFirstTabbable}>Focus First Tabbable</button>
73
{children}
74
</div>
75
);
76
}
77
78
// Modal focus management
79
function Modal({ isOpen, children }) {
80
const modalRef = useRef<HTMLDivElement>(null);
81
82
useEffect(() => {
83
if (isOpen && modalRef.current) {
84
// Focus first tabbable element when modal opens
85
const tabbable = Array.from(modalRef.current.querySelectorAll('*'))
86
.filter(isTabbable);
87
88
if (tabbable.length > 0) {
89
focusWithoutScrolling(tabbable[0] as FocusableElement);
90
}
91
}
92
}, [isOpen]);
93
94
return isOpen ? (
95
<div ref={modalRef} role="dialog">
96
{children}
97
</div>
98
) : null;
99
}
100
```
101
102
### ARIA Labeling
103
104
Utilities for managing ARIA labels and descriptions.
105
106
```typescript { .api }
107
/**
108
* Processes aria-label and aria-labelledby attributes
109
* @param props - Props containing labeling attributes
110
* @param defaultLabel - Fallback label when none provided
111
* @returns Props with processed labeling attributes
112
*/
113
function useLabels(
114
props: AriaLabelingProps,
115
defaultLabel?: string
116
): DOMProps & AriaLabelingProps;
117
118
/**
119
* Manages aria-describedby attributes
120
* @param description - Description text to associate with element
121
* @returns Props with aria-describedby attribute
122
*/
123
function useDescription(description: string | undefined): DOMProps & AriaLabelingProps;
124
125
interface AriaLabelingProps {
126
"aria-label"?: string;
127
"aria-labelledby"?: string;
128
"aria-describedby"?: string;
129
"aria-details"?: string;
130
}
131
```
132
133
**Usage Examples:**
134
135
```typescript
136
import { useLabels, useDescription, useId } from "@react-aria/utils";
137
138
function LabeledInput({ label, description, placeholder, ...props }) {
139
const inputId = useId();
140
141
// Handle labeling - creates element if aria-label provided
142
const labelProps = useLabels({
143
"aria-label": label,
144
...props
145
}, placeholder);
146
147
// Handle description - creates element for description text
148
const descriptionProps = useDescription(description);
149
150
// Merge all labeling props
151
const finalProps = {
152
id: inputId,
153
...labelProps,
154
...descriptionProps,
155
placeholder
156
};
157
158
return (
159
<div>
160
{/* Label element created by useLabels if needed */}
161
<input {...finalProps} />
162
{/* Description element created by useDescription if needed */}
163
</div>
164
);
165
}
166
167
// Usage
168
<LabeledInput
169
aria-label="Search query"
170
description="Enter keywords to search the catalog"
171
placeholder="Search..."
172
/>
173
```
174
175
### Advanced Labeling Patterns
176
177
Complex labeling scenarios with multiple label sources:
178
179
```typescript
180
import { useLabels, useDescription, useId, mergeIds } from "@react-aria/utils";
181
182
function ComplexForm() {
183
const fieldId = useId();
184
const groupId = useId();
185
186
return (
187
<fieldset>
188
<legend id={groupId}>Personal Information</legend>
189
190
<LabeledField
191
id={fieldId}
192
label="Full Name"
193
description="Enter your first and last name"
194
groupLabelId={groupId}
195
required
196
/>
197
</fieldset>
198
);
199
}
200
201
function LabeledField({
202
id,
203
label,
204
description,
205
groupLabelId,
206
required,
207
...props
208
}) {
209
const labelId = useId();
210
const defaultId = useId();
211
const finalId = mergeIds(defaultId, id);
212
213
// Create labeling props with multiple sources
214
const labelingProps = useLabels({
215
"aria-labelledby": mergeIds(groupLabelId, labelId),
216
"aria-label": !label ? `${required ? 'Required' : 'Optional'} field` : undefined
217
});
218
219
const descriptionProps = useDescription(description);
220
221
return (
222
<div>
223
<label id={labelId} htmlFor={finalId}>
224
{label}
225
{required && <span aria-hidden="true">*</span>}
226
</label>
227
<input
228
id={finalId}
229
required={required}
230
{...labelingProps}
231
{...descriptionProps}
232
{...props}
233
/>
234
</div>
235
);
236
}
237
```
238
239
### Focus Trap Implementation
240
241
Using focus utilities to create a focus trap:
242
243
```typescript
244
import { isTabbable, focusWithoutScrolling } from "@react-aria/utils";
245
246
function useFocusTrap(isActive: boolean) {
247
const containerRef = useRef<HTMLElement>(null);
248
249
useEffect(() => {
250
if (!isActive || !containerRef.current) return;
251
252
const container = containerRef.current;
253
const tabbableElements = Array.from(container.querySelectorAll('*'))
254
.filter(isTabbable) as FocusableElement[];
255
256
if (tabbableElements.length === 0) return;
257
258
const firstTabbable = tabbableElements[0];
259
const lastTabbable = tabbableElements[tabbableElements.length - 1];
260
261
const handleKeyDown = (e: KeyboardEvent) => {
262
if (e.key !== 'Tab') return;
263
264
if (e.shiftKey) {
265
// Shift+Tab: focus last element if currently on first
266
if (document.activeElement === firstTabbable) {
267
e.preventDefault();
268
focusWithoutScrolling(lastTabbable);
269
}
270
} else {
271
// Tab: focus first element if currently on last
272
if (document.activeElement === lastTabbable) {
273
e.preventDefault();
274
focusWithoutScrolling(firstTabbable);
275
}
276
}
277
};
278
279
// Focus first element initially
280
focusWithoutScrolling(firstTabbable);
281
282
container.addEventListener('keydown', handleKeyDown);
283
return () => container.removeEventListener('keydown', handleKeyDown);
284
}, [isActive]);
285
286
return containerRef;
287
}
288
289
// Usage in modal
290
function Modal({ isOpen, onClose, children }) {
291
const trapRef = useFocusTrap(isOpen);
292
293
return isOpen ? (
294
<div ref={trapRef} role="dialog" aria-modal="true">
295
<button onClick={onClose}>Close</button>
296
{children}
297
</div>
298
) : null;
299
}
300
```
301
302
## Types
303
304
```typescript { .api }
305
interface DOMProps {
306
id?: string;
307
}
308
309
interface AriaLabelingProps {
310
"aria-label"?: string;
311
"aria-labelledby"?: string;
312
"aria-describedby"?: string;
313
"aria-details"?: string;
314
}
315
316
type FocusableElement = HTMLElement | SVGElement;
317
```