Comprehensive accessibility utilities for focus management, tabbable elements, and ARIA attributes. These utilities follow WAI-ARIA guidelines and support keyboard navigation patterns essential for building accessible React applications.
Utilities for detecting focusable and tabbable elements.
/**
* Checks if an element is focusable (can receive focus programmatically)
* @param element - Element to check
* @returns true if element can be focused
*/
function isFocusable(element: HTMLElement): boolean;
/**
* Checks if an element is tabbable (can be reached via Tab key)
* @param element - Element to check (optional)
* @returns true if element is tabbable
*/
function isTabbable(element?: HTMLElement | null): boolean;
/**
* Checks if element has display: none
* @param element - Element to check
* @returns true if element has display: none
*/
function hasDisplayNone(element: HTMLElement): boolean;
/**
* Checks if element has a tabindex attribute
* @param element - Element to check
* @returns true if element has tabindex
*/
function hasTabIndex(element: HTMLElement): boolean;
/**
* Checks if element has a negative tabindex
* @param element - Element to check
* @returns true if element has negative tabindex
*/
function hasNegativeTabIndex(element: HTMLElement): boolean;
/**
* Checks if element has focus-within
* @param element - Element to check
* @returns true if element or its children have focus
*/
function hasFocusWithin(element: HTMLElement): boolean;Usage Examples:
import { isFocusable, isTabbable, hasFocusWithin } from "@chakra-ui/utils";
// Focus management in a modal
function Modal({ children, isOpen }: ModalProps) {
const modalRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
if (isOpen && modalRef.current) {
// Find first focusable element and focus it
const focusableElements = Array.from(modalRef.current.querySelectorAll("*"))
.filter((el): el is HTMLElement => el instanceof HTMLElement)
.filter(isFocusable);
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
}
}, [isOpen]);
return isOpen ? <div ref={modalRef}>{children}</div> : null;
}
// Custom focus trap
function useFocusTrap(ref: React.RefObject<HTMLElement>) {
React.useEffect(() => {
const container = ref.current;
if (!container) return;
function handleTab(event: KeyboardEvent) {
if (event.key !== "Tab") return;
const tabbableElements = Array.from(container.querySelectorAll("*"))
.filter((el): el is HTMLElement => el instanceof HTMLElement)
.filter(isTabbable);
const firstTabbable = tabbableElements[0];
const lastTabbable = tabbableElements[tabbableElements.length - 1];
if (event.shiftKey && document.activeElement === firstTabbable) {
event.preventDefault();
lastTabbable?.focus();
} else if (!event.shiftKey && document.activeElement === lastTabbable) {
event.preventDefault();
firstTabbable?.focus();
}
}
container.addEventListener("keydown", handleTab);
return () => container.removeEventListener("keydown", handleTab);
}, [ref]);
}Utilities for finding focusable and tabbable elements within containers.
/**
* Gets all focusable elements within a container
* @param container - Container element to search within
* @returns Array of focusable elements
*/
function getAllFocusable<T extends HTMLElement>(container: T): T[];
/**
* Gets the first focusable element within a container
* @param container - Container element to search within
* @returns First focusable element or null
*/
function getFirstFocusable<T extends HTMLElement>(container: T): T | null;
/**
* Gets all tabbable elements within a container
* @param container - Container element to search within
* @param fallbackToFocusable - Whether to fallback to focusable elements if no tabbable found
* @returns Array of tabbable elements
*/
function getAllTabbable<T extends HTMLElement>(
container: T,
fallbackToFocusable?: boolean
): T[];
/**
* Gets the first tabbable element within a container
* @param container - Container element to search within
* @param fallbackToFocusable - Whether to fallback to focusable elements if no tabbable found
* @returns First tabbable element or null
*/
function getFirstTabbableIn<T extends HTMLElement>(
container: T,
fallbackToFocusable?: boolean
): T | null;
/**
* Gets the last tabbable element within a container
* @param container - Container element to search within
* @param fallbackToFocusable - Whether to fallback to focusable elements if no tabbable found
* @returns Last tabbable element or null
*/
function getLastTabbableIn<T extends HTMLElement>(
container: T,
fallbackToFocusable?: boolean
): T | null;
/**
* Gets the next tabbable element after the currently focused element
* @param container - Container element to search within
* @param fallbackToFocusable - Whether to fallback to focusable elements if no tabbable found
* @returns Next tabbable element or null
*/
function getNextTabbable<T extends HTMLElement>(
container: T,
fallbackToFocusable?: boolean
): T | null;
/**
* Gets the previous tabbable element before the currently focused element
* @param container - Container element to search within
* @param fallbackToFocusable - Whether to fallback to focusable elements if no tabbable found
* @returns Previous tabbable element or null
*/
function getPreviousTabbable<T extends HTMLElement>(
container: T,
fallbackToFocusable?: boolean
): T | null;Usage Examples:
import {
getAllFocusable,
getFirstTabbableIn,
getLastTabbableIn,
getNextTabbable,
getPreviousTabbable
} from "@chakra-ui/utils";
// Dropdown with keyboard navigation
function Dropdown({ isOpen, options, onSelect }: DropdownProps) {
const listRef = React.useRef<HTMLUListElement>(null);
const [focusedIndex, setFocusedIndex] = React.useState(-1);
React.useEffect(() => {
if (isOpen && listRef.current) {
const firstTabbable = getFirstTabbableIn(listRef.current);
firstTabbable?.focus();
setFocusedIndex(0);
}
}, [isOpen]);
const handleKeyDown = (event: React.KeyboardEvent) => {
if (!listRef.current) return;
switch (event.key) {
case "ArrowDown":
event.preventDefault();
const nextElement = getNextTabbable(listRef.current);
if (nextElement) {
nextElement.focus();
setFocusedIndex(prev => prev + 1);
}
break;
case "ArrowUp":
event.preventDefault();
const prevElement = getPreviousTabbable(listRef.current);
if (prevElement) {
prevElement.focus();
setFocusedIndex(prev => prev - 1);
}
break;
case "Home":
event.preventDefault();
const firstElement = getFirstTabbableIn(listRef.current);
if (firstElement) {
firstElement.focus();
setFocusedIndex(0);
}
break;
case "End":
event.preventDefault();
const lastElement = getLastTabbableIn(listRef.current);
if (lastElement) {
lastElement.focus();
setFocusedIndex(options.length - 1);
}
break;
}
};
return (
<ul ref={listRef} onKeyDown={handleKeyDown}>
{options.map((option, index) => (
<li key={option.id} tabIndex={-1} onClick={() => onSelect(option)}>
{option.label}
</li>
))}
</ul>
);
}
// Focus restoration utility
function useFocusRestore() {
const previouslyFocusedRef = React.useRef<HTMLElement | null>(null);
const saveFocus = React.useCallback(() => {
previouslyFocusedRef.current = document.activeElement as HTMLElement;
}, []);
const restoreFocus = React.useCallback(() => {
const elementToFocus = previouslyFocusedRef.current;
if (elementToFocus && isFocusable(elementToFocus)) {
elementToFocus.focus();
}
}, []);
return { saveFocus, restoreFocus };
}Utilities for detecting various element states relevant to accessibility.
/**
* Type guard that checks if an element is an HTMLElement
* @param el - Element to check
* @returns true if element is HTMLElement
*/
function isHTMLElement(el: any): el is HTMLElement;
/**
* Checks if running in browser environment
* @returns true if in browser environment
*/
function isBrowser(): boolean;
/**
* Type guard that checks if an element is an input element
* @param element - Focusable element to check
* @returns true if element is HTMLInputElement
*/
function isInputElement(element: FocusableElement): element is HTMLInputElement;
/**
* Checks if an element is currently the active element
* @param element - Focusable element to check
* @returns true if element is currently focused
*/
function isActiveElement(element: FocusableElement): boolean;
/**
* Checks if an element is hidden (not visible)
* @param element - Element to check
* @returns true if element is hidden
*/
function isHiddenElement(element: HTMLElement): boolean;
/**
* Checks if an element is content editable
* @param element - Element to check
* @returns true if element is content editable
*/
function isContentEditableElement(element: HTMLElement): boolean;
/**
* Checks if an element is disabled
* @param element - Element to check
* @returns true if element is disabled
*/
function isDisabledElement(element: HTMLElement): boolean;Usage Examples:
import {
isHTMLElement,
isInputElement,
isActiveElement,
isHiddenElement,
isDisabledElement
} from "@chakra-ui/utils";
// Form validation helper
function validateFormElement(element: unknown): string | null {
if (!isHTMLElement(element)) {
return "Not a valid HTML element";
}
if (isHiddenElement(element)) {
return "Element is hidden";
}
if (isDisabledElement(element)) {
return "Element is disabled";
}
if (isInputElement(element) && element.required && !element.value) {
return "Required field is empty";
}
return null;
}
// Focus indicator component
function FocusIndicator({ children }: { children: React.ReactNode }) {
const [hasFocus, setHasFocus] = React.useState(false);
const elementRef = React.useRef<HTMLElement>(null);
React.useEffect(() => {
const checkFocus = () => {
if (elementRef.current) {
setHasFocus(isActiveElement(elementRef.current));
}
};
document.addEventListener("focusin", checkFocus);
document.addEventListener("focusout", checkFocus);
return () => {
document.removeEventListener("focusin", checkFocus);
document.removeEventListener("focusout", checkFocus);
};
}, []);
return (
<div
ref={elementRef as any}
className={cx("focus-container", hasFocus && "focus-container--focused")}
>
{children}
</div>
);
}Utilities for generating proper ARIA and data attributes.
/**
* Returns appropriate data attribute value based on condition
* @param condition - Boolean condition
* @returns Empty string for true, undefined for false/undefined
*/
function dataAttr(condition: boolean | undefined): Booleanish;
/**
* Returns appropriate ARIA attribute value based on condition
* @param condition - Boolean condition
* @returns true or undefined based on condition
*/
function ariaAttr(condition: boolean | undefined): true | undefined;
type Booleanish = boolean | "true" | "false";Usage Examples:
import { dataAttr, ariaAttr } from "@chakra-ui/utils";
// Accessible button component
interface ButtonProps {
children: React.ReactNode;
isPressed?: boolean;
isExpanded?: boolean;
isDisabled?: boolean;
onClick?: () => void;
}
function AccessibleButton({
children,
isPressed,
isExpanded,
isDisabled,
onClick
}: ButtonProps) {
return (
<button
onClick={onClick}
disabled={isDisabled}
aria-pressed={ariaAttr(isPressed)}
aria-expanded={ariaAttr(isExpanded)}
aria-disabled={ariaAttr(isDisabled)}
data-pressed={dataAttr(isPressed)}
data-expanded={dataAttr(isExpanded)}
data-disabled={dataAttr(isDisabled)}
>
{children}
</button>
);
}
// Accessible menu component
function Menu({ isOpen, items }: MenuProps) {
return (
<div
role="menu"
aria-expanded={ariaAttr(isOpen)}
data-open={dataAttr(isOpen)}
>
{items.map((item, index) => (
<div
key={item.id}
role="menuitem"
tabIndex={-1}
aria-disabled={ariaAttr(item.disabled)}
data-disabled={dataAttr(item.disabled)}
data-selected={dataAttr(item.selected)}
>
{item.label}
</div>
))}
</div>
);
}
// CSS styling based on data attributes
const styles = `
.button[data-pressed="true"] {
background-color: var(--pressed-bg);
}
.menu[data-open="true"] {
display: block;
}
.menu-item[data-disabled="true"] {
opacity: 0.5;
pointer-events: none;
}
`;