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
Support for aria-activedescendant focus patterns commonly used in comboboxes, listboxes, and other composite widgets where focus remains on a container while a descendant is highlighted.
Moves virtual focus from the current element to a target element, properly dispatching blur and focus events.
/**
* Moves virtual focus from current element to target element.
* Dispatches appropriate virtual blur and focus events.
*/
function moveVirtualFocus(to: Element | null): void;Usage Examples:
import React, { useRef, useState } from "react";
import { moveVirtualFocus } from "@react-aria/focus";
// Listbox with virtual focus
function VirtualListbox({ options, value, onChange }) {
const listboxRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState(0);
const optionRefs = useRef<(HTMLDivElement | null)[]>([]);
const moveToOption = (index: number) => {
if (index >= 0 && index < options.length) {
const option = optionRefs.current[index];
if (option) {
moveVirtualFocus(option);
setActiveIndex(index);
// Update aria-activedescendant on the listbox
if (listboxRef.current) {
listboxRef.current.setAttribute('aria-activedescendant', option.id);
}
}
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
moveToOption(Math.min(activeIndex + 1, options.length - 1));
break;
case 'ArrowUp':
e.preventDefault();
moveToOption(Math.max(activeIndex - 1, 0));
break;
case 'Enter':
case ' ':
e.preventDefault();
onChange(options[activeIndex]);
break;
}
};
return (
<div
ref={listboxRef}
role="listbox"
tabIndex={0}
onKeyDown={handleKeyDown}
aria-activedescendant={`option-${activeIndex}`}
>
{options.map((option, index) => (
<div
key={index}
ref={(el) => (optionRefs.current[index] = el)}
id={`option-${index}`}
role="option"
aria-selected={value === option}
onClick={() => {
moveToOption(index);
onChange(option);
}}
>
{option}
</div>
))}
</div>
);
}
// Combobox with virtual focus
function VirtualCombobox({ options, value, onChange }) {
const [isOpen, setIsOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const inputRef = useRef<HTMLInputElement>(null);
const optionRefs = useRef<(HTMLDivElement | null)[]>([]);
const moveToOption = (index: number) => {
if (index >= 0 && index < options.length) {
const option = optionRefs.current[index];
if (option) {
moveVirtualFocus(option);
setActiveIndex(index);
// Update aria-activedescendant on the input
if (inputRef.current) {
inputRef.current.setAttribute('aria-activedescendant', option.id);
}
}
} else {
// Clear virtual focus
moveVirtualFocus(null);
setActiveIndex(-1);
if (inputRef.current) {
inputRef.current.removeAttribute('aria-activedescendant');
}
}
};
return (
<div className="combobox">
<input
ref={inputRef}
type="text"
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
value={value}
onChange={(e) => onChange(e.target.value)}
onFocus={() => setIsOpen(true)}
onKeyDown={(e) => {
if (!isOpen) return;
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
moveToOption(activeIndex + 1);
break;
case 'ArrowUp':
e.preventDefault();
moveToOption(activeIndex - 1);
break;
case 'Enter':
if (activeIndex >= 0) {
e.preventDefault();
onChange(options[activeIndex]);
setIsOpen(false);
}
break;
case 'Escape':
setIsOpen(false);
moveToOption(-1);
break;
}
}}
/>
{isOpen && (
<div role="listbox">
{options.map((option, index) => (
<div
key={index}
ref={(el) => (optionRefs.current[index] = el)}
id={`combobox-option-${index}`}
role="option"
onClick={() => {
onChange(option);
setIsOpen(false);
}}
>
{option}
</div>
))}
</div>
)}
</div>
);
}Dispatches virtual blur events on an element when virtual focus is moving away from it.
/**
* Dispatches virtual blur events on element when virtual focus moves away.
*/
function dispatchVirtualBlur(from: Element, to: Element | null): void;Dispatches virtual focus events on an element when virtual focus is moving to it.
/**
* Dispatches virtual focus events on element when virtual focus moves to it.
*/
function dispatchVirtualFocus(to: Element, from: Element | null): void;Usage Examples:
import React, { useRef } from "react";
import { dispatchVirtualBlur, dispatchVirtualFocus } from "@react-aria/focus";
// Custom virtual focus implementation
function CustomVirtualFocus({ items }) {
const [focusedIndex, setFocusedIndex] = useState(-1);
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
const previousFocusedRef = useRef<Element | null>(null);
const setVirtualFocus = (index: number) => {
const previousElement = previousFocusedRef.current;
const newElement = index >= 0 ? itemRefs.current[index] : null;
// Dispatch blur event on previously focused element
if (previousElement && previousElement !== newElement) {
dispatchVirtualBlur(previousElement, newElement);
}
// Dispatch focus event on newly focused element
if (newElement && newElement !== previousElement) {
dispatchVirtualFocus(newElement, previousElement);
}
previousFocusedRef.current = newElement;
setFocusedIndex(index);
};
return (
<div>
{items.map((item, index) => (
<div
key={index}
ref={(el) => (itemRefs.current[index] = el)}
onVirtualFocus={() => console.log(`Virtual focus on ${item}`)}
onVirtualBlur={() => console.log(`Virtual blur from ${item}`)}
onClick={() => setVirtualFocus(index)}
style={{
backgroundColor: focusedIndex === index ? '#e0e0e0' : 'transparent'
}}
>
{item}
</div>
))}
<button onClick={() => setVirtualFocus(-1)}>Clear Virtual Focus</button>
</div>
);
}
// Event listener example
function VirtualFocusEventListener() {
const itemRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const element = itemRef.current;
if (!element) return;
const handleVirtualFocus = (e: Event) => {
console.log('Virtual focus received:', e);
element.classList.add('virtually-focused');
};
const handleVirtualBlur = (e: Event) => {
console.log('Virtual blur received:', e);
element.classList.remove('virtually-focused');
};
element.addEventListener('focus', handleVirtualFocus);
element.addEventListener('blur', handleVirtualBlur);
return () => {
element.removeEventListener('focus', handleVirtualFocus);
element.removeEventListener('blur', handleVirtualBlur);
};
}, []);
return (
<div>
<div ref={itemRef}>Virtual focusable item</div>
<button
onClick={() => dispatchVirtualFocus(itemRef.current!, null)}
>
Focus Item
</button>
<button
onClick={() => dispatchVirtualBlur(itemRef.current!, null)}
>
Blur Item
</button>
</div>
);
}Gets the currently virtually focused element using aria-activedescendant or the active element.
/**
* Gets currently virtually focused element using aria-activedescendant
* or falls back to the active element.
*/
function getVirtuallyFocusedElement(document: Document): Element | null;Usage Examples:
import React, { useEffect, useState } from "react";
import { getVirtuallyFocusedElement } from "@react-aria/focus";
// Virtual focus tracker
function VirtualFocusTracker() {
const [virtuallyFocused, setVirtuallyFocused] = useState<Element | null>(null);
useEffect(() => {
const updateVirtualFocus = () => {
const element = getVirtuallyFocusedElement(document);
setVirtuallyFocused(element);
};
// Update on focus changes
document.addEventListener('focusin', updateVirtualFocus);
document.addEventListener('focusout', updateVirtualFocus);
// Update when aria-activedescendant changes
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' &&
mutation.attributeName === 'aria-activedescendant') {
updateVirtualFocus();
}
}
});
observer.observe(document.body, {
attributes: true,
subtree: true,
attributeFilter: ['aria-activedescendant']
});
updateVirtualFocus();
return () => {
document.removeEventListener('focusin', updateVirtualFocus);
document.removeEventListener('focusout', updateVirtualFocus);
observer.disconnect();
};
}, []);
return (
<div>
<p>Currently virtually focused element:</p>
<pre>{virtuallyFocused ? virtuallyFocused.outerHTML : 'None'}</pre>
</div>
);
}
// Focus synchronization
function FocusSynchronizer({ onVirtualFocusChange }) {
useEffect(() => {
const checkVirtualFocus = () => {
const element = getVirtuallyFocusedElement(document);
onVirtualFocusChange(element);
};
// Polling approach for demonstration
const interval = setInterval(checkVirtualFocus, 100);
return () => clearInterval(interval);
}, [onVirtualFocusChange]);
return null;
}The virtual focus system supports the aria-activedescendant pattern where:
Listbox/Combobox:
Grid/TreeGrid:
Menu/Menubar:
Virtual focus events are regular DOM events:
focus event with relatedTarget set to previous elementblur event with relatedTarget set to next elementfocusin and focusout events that bubble normallypreventDefault()Virtual focus is announced by screen readers when:
Install with Tessl CLI
npx tessl i tessl/npm-react-aria--focus