or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

focus-navigation.mdfocus-ring.mdfocus-scope.mdindex.mdvirtual-focus.md

focus-navigation.mddocs/

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