0
# Shadow DOM Support
1
2
Complete Shadow DOM traversal, manipulation, and compatibility utilities for working with web components and shadow roots.
3
4
## Capabilities
5
6
### Shadow Tree Walker
7
8
Class implementing TreeWalker interface with Shadow DOM traversal support.
9
10
```typescript { .api }
11
/**
12
* TreeWalker implementation with Shadow DOM support
13
* Implements the full TreeWalker interface for traversing DOM trees
14
* that may contain Shadow DOM boundaries
15
*/
16
class ShadowTreeWalker implements TreeWalker {
17
readonly root: Node;
18
readonly whatToShow: number;
19
readonly filter: NodeFilter | null;
20
currentNode: Node;
21
22
parentNode(): Node | null;
23
firstChild(): Node | null;
24
lastChild(): Node | null;
25
previousSibling(): Node | null;
26
nextSibling(): Node | null;
27
previousNode(): Node | null;
28
nextNode(): Node | null;
29
}
30
31
/**
32
* Creates Shadow DOM-aware TreeWalker
33
* @param doc - Document context
34
* @param root - Root node to traverse from
35
* @param whatToShow - Node types to show (NodeFilter constants)
36
* @param filter - Optional node filter
37
* @returns TreeWalker implementation with shadow DOM support
38
*/
39
function createShadowTreeWalker(
40
doc: Document,
41
root: Node,
42
whatToShow?: number,
43
filter?: NodeFilter | null
44
): TreeWalker;
45
```
46
47
**Usage Examples:**
48
49
```typescript
50
import { ShadowTreeWalker, createShadowTreeWalker } from "@react-aria/utils";
51
52
// Traverse entire DOM tree including shadow DOM
53
function traverseAllNodes(rootElement: Element) {
54
const walker = createShadowTreeWalker(
55
document,
56
rootElement,
57
NodeFilter.SHOW_ELEMENT
58
);
59
60
const allElements: Element[] = [];
61
let currentNode = walker.currentNode as Element;
62
63
// Collect all elements including those in shadow DOM
64
do {
65
allElements.push(currentNode);
66
currentNode = walker.nextNode() as Element;
67
} while (currentNode);
68
69
return allElements;
70
}
71
72
// Find focusable elements across shadow boundaries
73
function findAllFocusableElements(container: Element): Element[] {
74
const walker = createShadowTreeWalker(
75
document,
76
container,
77
NodeFilter.SHOW_ELEMENT,
78
{
79
acceptNode: (node) => {
80
const element = node as Element;
81
82
// Check if element is focusable
83
if (element.matches('button, input, select, textarea, a[href], [tabindex]')) {
84
return NodeFilter.FILTER_ACCEPT;
85
}
86
87
return NodeFilter.FILTER_SKIP;
88
}
89
}
90
);
91
92
const focusableElements: Element[] = [];
93
let currentNode = walker.nextNode() as Element;
94
95
while (currentNode) {
96
focusableElements.push(currentNode);
97
currentNode = walker.nextNode() as Element;
98
}
99
100
return focusableElements;
101
}
102
103
// Custom TreeWalker with shadow DOM support
104
function createCustomWalker(root: Element, filterFunction: (node: Element) => boolean) {
105
return createShadowTreeWalker(
106
document,
107
root,
108
NodeFilter.SHOW_ELEMENT,
109
{
110
acceptNode: (node) => {
111
return filterFunction(node as Element)
112
? NodeFilter.FILTER_ACCEPT
113
: NodeFilter.FILTER_SKIP;
114
}
115
}
116
);
117
}
118
```
119
120
### Shadow DOM Utilities
121
122
Functions for safely working with Shadow DOM elements and events.
123
124
```typescript { .api }
125
/**
126
* Shadow DOM-safe version of document.activeElement
127
* @param doc - Document to get active element from (default: document)
128
* @returns Currently focused element, traversing into shadow roots
129
*/
130
function getActiveElement(doc?: Document): Element | null;
131
132
/**
133
* Shadow DOM-safe version of event.target
134
* @param event - Event to get target from
135
* @returns Event target, using composedPath() when available
136
*/
137
function getEventTarget<T extends Event>(event: T): Element | null;
138
139
/**
140
* Shadow DOM-safe version of Node.contains()
141
* @param node - Container node
142
* @param otherNode - Node to check containment for
143
* @returns Boolean indicating containment across shadow boundaries
144
*/
145
function nodeContains(node: Node, otherNode: Node): boolean;
146
```
147
148
**Usage Examples:**
149
150
```typescript
151
import { getActiveElement, getEventTarget, nodeContains } from "@react-aria/utils";
152
153
// Focus management across shadow boundaries
154
function FocusManager({ children }) {
155
const containerRef = useRef<HTMLDivElement>(null);
156
157
const handleFocusOut = (e: FocusEvent) => {
158
// Get the actual focused element (may be in shadow DOM)
159
const activeElement = getActiveElement();
160
const container = containerRef.current;
161
162
if (container && activeElement) {
163
// Check if focus moved outside container (across shadow boundaries)
164
if (!nodeContains(container, activeElement)) {
165
console.log('Focus moved outside container');
166
onFocusLeave();
167
}
168
}
169
};
170
171
const handleGlobalClick = (e: MouseEvent) => {
172
// Get actual click target (may be in shadow DOM)
173
const target = getEventTarget(e);
174
const container = containerRef.current;
175
176
if (container && target) {
177
// Check if click was outside container
178
if (!nodeContains(container, target)) {
179
console.log('Clicked outside container');
180
onClickOutside();
181
}
182
}
183
};
184
185
useEffect(() => {
186
document.addEventListener('click', handleGlobalClick);
187
return () => document.removeEventListener('click', handleGlobalClick);
188
}, []);
189
190
return (
191
<div ref={containerRef} onFocusOut={handleFocusOut}>
192
{children}
193
</div>
194
);
195
}
196
197
// Modal with shadow DOM support
198
function Modal({ isOpen, onClose, children }) {
199
const modalRef = useRef<HTMLDivElement>(null);
200
201
useEffect(() => {
202
if (!isOpen) return;
203
204
const handleEscape = (e: KeyboardEvent) => {
205
if (e.key === 'Escape') {
206
onClose();
207
}
208
};
209
210
const handleClickOutside = (e: MouseEvent) => {
211
const target = getEventTarget(e);
212
const modal = modalRef.current;
213
214
if (modal && target && !nodeContains(modal, target)) {
215
onClose();
216
}
217
};
218
219
document.addEventListener('keydown', handleEscape);
220
document.addEventListener('mousedown', handleClickOutside);
221
222
return () => {
223
document.removeEventListener('keydown', handleEscape);
224
document.removeEventListener('mousedown', handleClickOutside);
225
};
226
}, [isOpen, onClose]);
227
228
// Focus first element when modal opens
229
useEffect(() => {
230
if (isOpen && modalRef.current) {
231
const focusableElements = findAllFocusableElements(modalRef.current);
232
if (focusableElements.length > 0) {
233
(focusableElements[0] as HTMLElement).focus();
234
}
235
}
236
}, [isOpen]);
237
238
return isOpen ? (
239
<div className="modal-backdrop">
240
<div ref={modalRef} className="modal-content" role="dialog" aria-modal="true">
241
{children}
242
</div>
243
</div>
244
) : null;
245
}
246
247
// Dropdown menu with shadow DOM event handling
248
function DropdownMenu({ trigger, children }) {
249
const [isOpen, setIsOpen] = useState(false);
250
const containerRef = useRef<HTMLDivElement>(null);
251
252
useEffect(() => {
253
if (!isOpen) return;
254
255
const handleGlobalClick = (e: MouseEvent) => {
256
const target = getEventTarget(e);
257
const container = containerRef.current;
258
259
if (container && target && !nodeContains(container, target)) {
260
setIsOpen(false);
261
}
262
};
263
264
// Use capture phase to ensure we get the event before shadow DOM
265
document.addEventListener('mousedown', handleGlobalClick, true);
266
267
return () => {
268
document.removeEventListener('mousedown', handleGlobalClick, true);
269
};
270
}, [isOpen]);
271
272
return (
273
<div ref={containerRef} className="dropdown">
274
<div onClick={() => setIsOpen(!isOpen)}>
275
{trigger}
276
</div>
277
{isOpen && (
278
<div className="dropdown-menu">
279
{children}
280
</div>
281
)}
282
</div>
283
);
284
}
285
```
286
287
### Advanced Shadow DOM Patterns
288
289
Complex scenarios involving web components and shadow DOM boundaries:
290
291
```typescript
292
import {
293
createShadowTreeWalker,
294
getActiveElement,
295
getEventTarget,
296
nodeContains
297
} from "@react-aria/utils";
298
299
// Web component integration
300
function WebComponentWrapper({ children }) {
301
const wrapperRef = useRef<HTMLDivElement>(null);
302
303
const findElementsInShadowDOM = useCallback((selector: string) => {
304
if (!wrapperRef.current) return [];
305
306
const walker = createShadowTreeWalker(
307
document,
308
wrapperRef.current,
309
NodeFilter.SHOW_ELEMENT,
310
{
311
acceptNode: (node) => {
312
const element = node as Element;
313
return element.matches(selector)
314
? NodeFilter.FILTER_ACCEPT
315
: NodeFilter.FILTER_SKIP;
316
}
317
}
318
);
319
320
const elements: Element[] = [];
321
let currentNode = walker.nextNode() as Element;
322
323
while (currentNode) {
324
elements.push(currentNode);
325
currentNode = walker.nextNode() as Element;
326
}
327
328
return elements;
329
}, []);
330
331
const handleInteraction = (e: Event) => {
332
// Get the actual target even if it's in shadow DOM
333
const target = getEventTarget(e);
334
335
if (target) {
336
console.log('Interaction with element:', target.tagName);
337
338
// Find all related elements in shadow DOM
339
const relatedElements = findElementsInShadowDOM('[data-related]');
340
relatedElements.forEach(el => {
341
el.classList.add('highlighted');
342
});
343
}
344
};
345
346
return (
347
<div
348
ref={wrapperRef}
349
onMouseOver={handleInteraction}
350
onFocus={handleInteraction}
351
>
352
{children}
353
</div>
354
);
355
}
356
357
// Cross-shadow-boundary focus trap
358
function ShadowAwareFocusTrap({ isActive, children }) {
359
const containerRef = useRef<HTMLDivElement>(null);
360
361
useEffect(() => {
362
if (!isActive || !containerRef.current) return;
363
364
// Find all focusable elements including those in shadow DOM
365
const focusableElements = findAllFocusableElements(containerRef.current);
366
367
if (focusableElements.length === 0) return;
368
369
const firstFocusable = focusableElements[0] as HTMLElement;
370
const lastFocusable = focusableElements[focusableElements.length - 1] as HTMLElement;
371
372
const handleKeyDown = (e: KeyboardEvent) => {
373
if (e.key !== 'Tab') return;
374
375
const activeElement = getActiveElement();
376
377
if (e.shiftKey) {
378
// Shift+Tab: wrap to last if on first
379
if (activeElement === firstFocusable) {
380
e.preventDefault();
381
lastFocusable.focus();
382
}
383
} else {
384
// Tab: wrap to first if on last
385
if (activeElement === lastFocusable) {
386
e.preventDefault();
387
firstFocusable.focus();
388
}
389
}
390
};
391
392
// Focus first element initially
393
firstFocusable.focus();
394
395
document.addEventListener('keydown', handleKeyDown);
396
return () => document.removeEventListener('keydown', handleKeyDown);
397
}, [isActive]);
398
399
return (
400
<div ref={containerRef}>
401
{children}
402
</div>
403
);
404
}
405
406
// Event delegation across shadow boundaries
407
function ShadowAwareEventDelegation({ onButtonClick, children }) {
408
const containerRef = useRef<HTMLDivElement>(null);
409
410
useEffect(() => {
411
const container = containerRef.current;
412
if (!container) return;
413
414
const handleClick = (e: MouseEvent) => {
415
const target = getEventTarget(e);
416
417
if (target && target.matches('button, [role="button"]')) {
418
// Check if the button is contained within our container
419
if (nodeContains(container, target)) {
420
onButtonClick(target, e);
421
}
422
}
423
};
424
425
// Use capture to ensure we get events from shadow DOM
426
document.addEventListener('click', handleClick, true);
427
428
return () => {
429
document.removeEventListener('click', handleClick, true);
430
};
431
}, [onButtonClick]);
432
433
return (
434
<div ref={containerRef}>
435
{children}
436
</div>
437
);
438
}
439
```
440
441
## Performance Considerations
442
443
Shadow DOM utilities are designed for performance:
444
445
- **TreeWalker**: Uses native browser TreeWalker when possible, falls back to custom implementation
446
- **Event targeting**: Leverages `composedPath()` when available for efficient shadow DOM traversal
447
- **Containment checking**: Optimized algorithms for cross-boundary containment checks
448
- **Caching**: Active element and event target results are not cached to ensure accuracy
449
450
## Browser Compatibility
451
452
These utilities provide consistent behavior across all modern browsers:
453
454
- **Chrome/Edge**: Full native Shadow DOM support
455
- **Firefox**: Full native Shadow DOM support
456
- **Safari**: Full native Shadow DOM support
457
- **Older browsers**: Graceful fallback to standard DOM methods
458
459
## Types
460
461
```typescript { .api }
462
interface TreeWalker {
463
readonly root: Node;
464
readonly whatToShow: number;
465
readonly filter: NodeFilter | null;
466
currentNode: Node;
467
468
parentNode(): Node | null;
469
firstChild(): Node | null;
470
lastChild(): Node | null;
471
previousSibling(): Node | null;
472
nextSibling(): Node | null;
473
previousNode(): Node | null;
474
nextNode(): Node | null;
475
}
476
477
interface NodeFilter {
478
acceptNode(node: Node): number;
479
}
480
481
declare const NodeFilter: {
482
readonly FILTER_ACCEPT: 1;
483
readonly FILTER_REJECT: 2;
484
readonly FILTER_SKIP: 3;
485
readonly SHOW_ALL: 0xFFFFFFFF;
486
readonly SHOW_ELEMENT: 0x1;
487
readonly SHOW_ATTRIBUTE: 0x2;
488
readonly SHOW_TEXT: 0x4;
489
readonly SHOW_CDATA_SECTION: 0x8;
490
readonly SHOW_ENTITY_REFERENCE: 0x10;
491
readonly SHOW_ENTITY: 0x20;
492
readonly SHOW_PROCESSING_INSTRUCTION: 0x40;
493
readonly SHOW_COMMENT: 0x80;
494
readonly SHOW_DOCUMENT: 0x100;
495
readonly SHOW_DOCUMENT_TYPE: 0x200;
496
readonly SHOW_DOCUMENT_FRAGMENT: 0x400;
497
readonly SHOW_NOTATION: 0x800;
498
};
499
```