React hooks and components for accessible focus management including FocusScope for focus containment, FocusRing for visual focus indicators, and utilities for focus navigation and virtual focus handling.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Low-level utilities for creating custom focus managers, traversing focusable elements, and implementing keyboard navigation patterns in complex UI components.
Creates a FocusManager object for moving focus within a specific element, independent of FocusScope components.
/**
* Creates a FocusManager object that can be used to move focus within an element.
*/
function createFocusManager(
ref: RefObject<Element | null>,
defaultOptions?: FocusManagerOptions
): FocusManager;
interface FocusManager {
/** Moves focus to the next focusable or tabbable element in the focus scope. */
focusNext(opts?: FocusManagerOptions): FocusableElement | null;
/** Moves focus to the previous focusable or tabbable element in the focus scope. */
focusPrevious(opts?: FocusManagerOptions): FocusableElement | null;
/** Moves focus to the first focusable or tabbable element in the focus scope. */
focusFirst(opts?: FocusManagerOptions): FocusableElement | null;
/** Moves focus to the last focusable or tabbable element in the focus scope. */
focusLast(opts?: FocusManagerOptions): FocusableElement | null;
}
interface FocusManagerOptions {
/** The element to start searching from. The currently focused element by default. */
from?: Element;
/** Whether to only include tabbable elements, or all focusable elements. */
tabbable?: boolean;
/** Whether focus should wrap around when it reaches the end of the scope. */
wrap?: boolean;
/** A callback that determines whether the given element is focused. */
accept?: (node: Element) => boolean;
}Usage Examples:
import React, { useRef, useEffect } from "react";
import { createFocusManager } from "@react-aria/focus";
// Custom grid navigation
function NavigableGrid({ items, columns }) {
const gridRef = useRef<HTMLDivElement>(null);
const focusManager = useRef<FocusManager>();
useEffect(() => {
focusManager.current = createFocusManager(gridRef, {
tabbable: true,
wrap: false
});
}, []);
const handleKeyDown = (e: React.KeyboardEvent) => {
const manager = focusManager.current;
if (!manager) return;
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
manager.focusNext();
break;
case 'ArrowLeft':
e.preventDefault();
manager.focusPrevious();
break;
case 'Home':
e.preventDefault();
manager.focusFirst();
break;
case 'End':
e.preventDefault();
manager.focusLast();
break;
}
};
return (
<div
ref={gridRef}
className="grid"
style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
onKeyDown={handleKeyDown}
>
{items.map((item, index) => (
<button key={index} tabIndex={index === 0 ? 0 : -1}>
{item}
</button>
))}
</div>
);
}
// Custom focus manager with filtering
function FilteredNavigation({ items, isDisabled }) {
const containerRef = useRef<HTMLDivElement>(null);
const focusManager = useRef<FocusManager>();
useEffect(() => {
focusManager.current = createFocusManager(containerRef);
}, []);
const focusNextEnabled = () => {
focusManager.current?.focusNext({
accept: (node) => {
const index = parseInt(node.getAttribute('data-index') || '0');
return !isDisabled(items[index]);
}
});
};
const focusPreviousEnabled = () => {
focusManager.current?.focusPrevious({
accept: (node) => {
const index = parseInt(node.getAttribute('data-index') || '0');
return !isDisabled(items[index]);
}
});
};
return (
<div ref={containerRef}>
<button onClick={focusPreviousEnabled}>Previous Enabled</button>
<button onClick={focusNextEnabled}>Next Enabled</button>
{items.map((item, index) => (
<button
key={index}
data-index={index}
disabled={isDisabled(item)}
tabIndex={-1}
>
{item.name}
</button>
))}
</div>
);
}Creates a TreeWalker that matches all focusable or tabbable elements within a root element, with optional filtering.
/**
* Create a TreeWalker that matches all focusable/tabbable elements.
*/
function getFocusableTreeWalker(
root: Element,
opts?: FocusManagerOptions,
scope?: Element[]
): TreeWalker | ShadowTreeWalker;Usage Examples:
import React, { useRef } from "react";
import { getFocusableTreeWalker } from "@react-aria/focus";
// Find all focusable elements
function FocusableElementFinder() {
const containerRef = useRef<HTMLDivElement>(null);
const findFocusableElements = () => {
if (!containerRef.current) return [];
const walker = getFocusableTreeWalker(containerRef.current, {
tabbable: false // Include all focusable elements, not just tabbable ones
});
const elements: Element[] = [];
let node = walker.nextNode() as Element;
while (node) {
elements.push(node);
node = walker.nextNode() as Element;
}
return elements;
};
const logFocusableElements = () => {
const elements = findFocusableElements();
console.log('Focusable elements:', elements);
};
return (
<div ref={containerRef}>
<button onClick={logFocusableElements}>Find Focusable Elements</button>
<input type="text" placeholder="Focusable input" />
<button>Focusable button</button>
<div tabIndex={0}>Focusable div</div>
<a href="#" tabIndex={-1}>Non-tabbable link</a>
<button disabled>Disabled button</button>
</div>
);
}
// Count tabbable elements
function TabbableCounter() {
const containerRef = useRef<HTMLFormElement>(null);
const countTabbableElements = () => {
if (!containerRef.current) return 0;
const walker = getFocusableTreeWalker(containerRef.current, {
tabbable: true
});
let count = 0;
while (walker.nextNode()) {
count++;
}
return count;
};
return (
<form ref={containerRef}>
<p>Tabbable elements: {countTabbableElements()}</p>
<input type="text" />
<button type="button">Button</button>
<select>
<option>Option 1</option>
</select>
<textarea></textarea>
</form>
);
}
// Custom traversal with filtering
function FilteredTraversal() {
const containerRef = useRef<HTMLDivElement>(null);
const findButtonElements = () => {
if (!containerRef.current) return [];
const walker = getFocusableTreeWalker(containerRef.current, {
tabbable: true,
accept: (node) => node.tagName === 'BUTTON'
});
const buttons: Element[] = [];
let node = walker.nextNode() as Element;
while (node) {
buttons.push(node);
node = walker.nextNode() as Element;
}
return buttons;
};
return (
<div ref={containerRef}>
<input type="text" />
<button>Button 1</button>
<select><option>Select</option></select>
<button>Button 2</button>
<textarea></textarea>
<button>Button 3</button>
<p>Found {findButtonElements().length} buttons</p>
</div>
);
}Returns whether an element has a tabbable child and updates as children change.
/**
* Returns whether an element has a tabbable child, and updates as children change.
* @private - Internal utility for special cases
*/
function useHasTabbableChild(
ref: RefObject<Element | null>,
options?: AriaHasTabbableChildOptions
): boolean;
interface AriaHasTabbableChildOptions {
isDisabled?: boolean;
}Usage Examples:
import React, { useRef } from "react";
import { useHasTabbableChild } from "@react-aria/focus";
// Dynamic empty state handling
function CollectionContainer({ items, emptyMessage }) {
const containerRef = useRef<HTMLDivElement>(null);
const hasTabbableChild = useHasTabbableChild(containerRef);
// Show different tabIndex based on whether container has tabbable children
const containerTabIndex = hasTabbableChild ? -1 : 0;
return (
<div
ref={containerRef}
tabIndex={containerTabIndex}
role="grid"
aria-label={items.length === 0 ? "Empty collection" : "Collection"}
>
{items.length > 0 ? (
items.map((item, index) => (
<button key={index} role="gridcell">
{item.name}
</button>
))
) : (
<div role="gridcell">
{emptyMessage}
<button>Add Item</button>
</div>
)}
</div>
);
}
// Conditional keyboard navigation
function ConditionalNavigation({ isNavigationDisabled, children }) {
const containerRef = useRef<HTMLDivElement>(null);
const hasTabbableChild = useHasTabbableChild(containerRef, {
isDisabled: isNavigationDisabled
});
return (
<div
ref={containerRef}
data-has-tabbable-child={hasTabbableChild}
tabIndex={hasTabbableChild ? -1 : 0}
>
Navigation Status: {hasTabbableChild ? 'Has tabbable children' : 'No tabbable children'}
{children}
</div>
);
}The getFocusableTreeWalker function creates a DOM TreeWalker that:
Focusable Elements:
element.focus())tabIndex="-1"Tabbable Elements:
tabIndex >= 0 or are naturally tabbable (buttons, inputs, etc.)Common patterns supported by these utilities:
querySelectorAll for large DOMsuseHasTabbableChild automatically updates on DOM changestabbable: true when possible to reduce the search spaceInstall with Tessl CLI
npx tessl i tessl/npm-react-aria--focus