or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

dom-events.mddom-utilities.mdfocus-accessibility.mdfunction-utilities.mdindex.mdmath-utilities.mdobject-manipulation.mdreact-utilities.mdresponsive.mdtype-checking.md
tile.json

focus-accessibility.mddocs/

Focus and Accessibility

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.

Capabilities

Element Focus Detection

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]);
}

Focus Query Utilities

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 };
}

Element State Detection

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>
  );
}

ARIA Attribute Utilities

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;
  }
`;