CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-aria--utils

Essential utility functions and React hooks for building accessible React Aria UI components

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

focus-and-accessibility.mddocs/

Focus & Accessibility

Focus management, element accessibility checks, and ARIA labeling utilities for building accessible React components.

Capabilities

Focus Management

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

ARIA Labeling

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..."
/>

Advanced Labeling Patterns

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

Focus Trap Implementation

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

Types

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

docs

animation-and-transitions.md

event-management.md

focus-and-accessibility.md

id-and-refs.md

index.md

links-and-navigation.md

miscellaneous-utilities.md

platform-detection.md

props-and-events.md

scrolling-and-layout.md

shadow-dom-support.md

state-and-effects.md

virtual-events-and-input.md

tile.json