or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

animation-and-transitions.mdevent-management.mdfocus-and-accessibility.mdid-and-refs.mdindex.mdlinks-and-navigation.mdmiscellaneous-utilities.mdplatform-detection.mdprops-and-events.mdscrolling-and-layout.mdshadow-dom-support.mdstate-and-effects.mdvirtual-events-and-input.md

shadow-dom-support.mddocs/

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

```