Essential utility functions and React hooks for building accessible React Aria UI components
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Focus management, element accessibility checks, and ARIA labeling utilities for building accessible React components.
Utilities for managing focus behavior and determining focusability.
/**
* Focuses element without scrolling the page
* Polyfill for {preventScroll: true} option in older browsers
* @param element - Element to focus
*/
function focusWithoutScrolling(element: FocusableElement): void;
/**
* Determines if element can receive focus
* @param element - Element to check
* @returns true if element is focusable
*/
function isFocusable(element: Element): boolean;
/**
* Determines if element is reachable via Tab key
* @param element - Element to check
* @returns true if element is tabbable (excludes tabindex="-1")
*/
function isTabbable(element: Element): boolean;
type FocusableElement = HTMLElement | SVGElement;Usage Examples:
import { focusWithoutScrolling, isFocusable, isTabbable } from "@react-aria/utils";
function FocusManager({ children }) {
const containerRef = useRef<HTMLDivElement>(null);
const focusFirstElement = () => {
if (!containerRef.current) return;
// Find first focusable element
const focusableElements = Array.from(
containerRef.current.querySelectorAll('*')
).filter(isFocusable);
if (focusableElements.length > 0) {
focusWithoutScrolling(focusableElements[0] as FocusableElement);
}
};
const focusFirstTabbable = () => {
if (!containerRef.current) return;
// Find first tabbable element (keyboard accessible)
const tabbableElements = Array.from(
containerRef.current.querySelectorAll('*')
).filter(isTabbable);
if (tabbableElements.length > 0) {
focusWithoutScrolling(tabbableElements[0] as FocusableElement);
}
};
return (
<div ref={containerRef}>
<button onClick={focusFirstElement}>Focus First Focusable</button>
<button onClick={focusFirstTabbable}>Focus First Tabbable</button>
{children}
</div>
);
}
// Modal focus management
function Modal({ isOpen, children }) {
const modalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen && modalRef.current) {
// Focus first tabbable element when modal opens
const tabbable = Array.from(modalRef.current.querySelectorAll('*'))
.filter(isTabbable);
if (tabbable.length > 0) {
focusWithoutScrolling(tabbable[0] as FocusableElement);
}
}
}, [isOpen]);
return isOpen ? (
<div ref={modalRef} role="dialog">
{children}
</div>
) : null;
}Utilities for managing ARIA labels and descriptions.
/**
* Processes aria-label and aria-labelledby attributes
* @param props - Props containing labeling attributes
* @param defaultLabel - Fallback label when none provided
* @returns Props with processed labeling attributes
*/
function useLabels(
props: AriaLabelingProps,
defaultLabel?: string
): DOMProps & AriaLabelingProps;
/**
* Manages aria-describedby attributes
* @param description - Description text to associate with element
* @returns Props with aria-describedby attribute
*/
function useDescription(description: string | undefined): DOMProps & AriaLabelingProps;
interface AriaLabelingProps {
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
"aria-details"?: string;
}Usage Examples:
import { useLabels, useDescription, useId } from "@react-aria/utils";
function LabeledInput({ label, description, placeholder, ...props }) {
const inputId = useId();
// Handle labeling - creates element if aria-label provided
const labelProps = useLabels({
"aria-label": label,
...props
}, placeholder);
// Handle description - creates element for description text
const descriptionProps = useDescription(description);
// Merge all labeling props
const finalProps = {
id: inputId,
...labelProps,
...descriptionProps,
placeholder
};
return (
<div>
{/* Label element created by useLabels if needed */}
<input {...finalProps} />
{/* Description element created by useDescription if needed */}
</div>
);
}
// Usage
<LabeledInput
aria-label="Search query"
description="Enter keywords to search the catalog"
placeholder="Search..."
/>Complex labeling scenarios with multiple label sources:
import { useLabels, useDescription, useId, mergeIds } from "@react-aria/utils";
function ComplexForm() {
const fieldId = useId();
const groupId = useId();
return (
<fieldset>
<legend id={groupId}>Personal Information</legend>
<LabeledField
id={fieldId}
label="Full Name"
description="Enter your first and last name"
groupLabelId={groupId}
required
/>
</fieldset>
);
}
function LabeledField({
id,
label,
description,
groupLabelId,
required,
...props
}) {
const labelId = useId();
const defaultId = useId();
const finalId = mergeIds(defaultId, id);
// Create labeling props with multiple sources
const labelingProps = useLabels({
"aria-labelledby": mergeIds(groupLabelId, labelId),
"aria-label": !label ? `${required ? 'Required' : 'Optional'} field` : undefined
});
const descriptionProps = useDescription(description);
return (
<div>
<label id={labelId} htmlFor={finalId}>
{label}
{required && <span aria-hidden="true">*</span>}
</label>
<input
id={finalId}
required={required}
{...labelingProps}
{...descriptionProps}
{...props}
/>
</div>
);
}Using focus utilities to create a focus trap:
import { isTabbable, focusWithoutScrolling } from "@react-aria/utils";
function useFocusTrap(isActive: boolean) {
const containerRef = useRef<HTMLElement>(null);
useEffect(() => {
if (!isActive || !containerRef.current) return;
const container = containerRef.current;
const tabbableElements = Array.from(container.querySelectorAll('*'))
.filter(isTabbable) as FocusableElement[];
if (tabbableElements.length === 0) return;
const firstTabbable = tabbableElements[0];
const lastTabbable = tabbableElements[tabbableElements.length - 1];
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
// Shift+Tab: focus last element if currently on first
if (document.activeElement === firstTabbable) {
e.preventDefault();
focusWithoutScrolling(lastTabbable);
}
} else {
// Tab: focus first element if currently on last
if (document.activeElement === lastTabbable) {
e.preventDefault();
focusWithoutScrolling(firstTabbable);
}
}
};
// Focus first element initially
focusWithoutScrolling(firstTabbable);
container.addEventListener('keydown', handleKeyDown);
return () => container.removeEventListener('keydown', handleKeyDown);
}, [isActive]);
return containerRef;
}
// Usage in modal
function Modal({ isOpen, onClose, children }) {
const trapRef = useFocusTrap(isOpen);
return isOpen ? (
<div ref={trapRef} role="dialog" aria-modal="true">
<button onClick={onClose}>Close</button>
{children}
</div>
) : null;
}interface DOMProps {
id?: string;
}
interface AriaLabelingProps {
"aria-label"?: string;
"aria-labelledby"?: string;
"aria-describedby"?: string;
"aria-details"?: string;
}
type FocusableElement = HTMLElement | SVGElement;Install with Tessl CLI
npx tessl i tessl/npm-react-aria--utils