CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-aria--focus

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.

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

focus-navigation.mddocs/

Focus Navigation Utilities

Low-level utilities for creating custom focus managers, traversing focusable elements, and implementing keyboard navigation patterns in complex UI components.

Capabilities

createFocusManager Function

Creates a FocusManager object for moving focus within a specific element, independent of FocusScope components.

/**
 * Creates a FocusManager object that can be used to move focus within an element.
 */
function createFocusManager(
  ref: RefObject<Element | null>,
  defaultOptions?: FocusManagerOptions
): FocusManager;

interface FocusManager {
  /** Moves focus to the next focusable or tabbable element in the focus scope. */
  focusNext(opts?: FocusManagerOptions): FocusableElement | null;
  /** Moves focus to the previous focusable or tabbable element in the focus scope. */  
  focusPrevious(opts?: FocusManagerOptions): FocusableElement | null;
  /** Moves focus to the first focusable or tabbable element in the focus scope. */
  focusFirst(opts?: FocusManagerOptions): FocusableElement | null;
  /** Moves focus to the last focusable or tabbable element in the focus scope. */
  focusLast(opts?: FocusManagerOptions): FocusableElement | null;
}

interface FocusManagerOptions {
  /** The element to start searching from. The currently focused element by default. */
  from?: Element;
  /** Whether to only include tabbable elements, or all focusable elements. */
  tabbable?: boolean;
  /** Whether focus should wrap around when it reaches the end of the scope. */
  wrap?: boolean;
  /** A callback that determines whether the given element is focused. */
  accept?: (node: Element) => boolean;
}

Usage Examples:

import React, { useRef, useEffect } from "react";
import { createFocusManager } from "@react-aria/focus";

// Custom grid navigation
function NavigableGrid({ items, columns }) {
  const gridRef = useRef<HTMLDivElement>(null);
  const focusManager = useRef<FocusManager>();
  
  useEffect(() => {
    focusManager.current = createFocusManager(gridRef, {
      tabbable: true,
      wrap: false
    });
  }, []);
  
  const handleKeyDown = (e: React.KeyboardEvent) => {
    const manager = focusManager.current;
    if (!manager) return;
    
    switch (e.key) {
      case 'ArrowRight':
        e.preventDefault();
        manager.focusNext();
        break;
      case 'ArrowLeft':
        e.preventDefault();
        manager.focusPrevious();
        break;
      case 'Home':
        e.preventDefault();
        manager.focusFirst();
        break;
      case 'End':
        e.preventDefault();
        manager.focusLast();
        break;
    }
  };
  
  return (
    <div 
      ref={gridRef}
      className="grid"
      style={{ gridTemplateColumns: `repeat(${columns}, 1fr)` }}
      onKeyDown={handleKeyDown}
    >
      {items.map((item, index) => (
        <button key={index} tabIndex={index === 0 ? 0 : -1}>
          {item}
        </button>
      ))}
    </div>
  );
}

// Custom focus manager with filtering
function FilteredNavigation({ items, isDisabled }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const focusManager = useRef<FocusManager>();
  
  useEffect(() => {
    focusManager.current = createFocusManager(containerRef);
  }, []);
  
  const focusNextEnabled = () => {
    focusManager.current?.focusNext({
      accept: (node) => {
        const index = parseInt(node.getAttribute('data-index') || '0');
        return !isDisabled(items[index]);
      }
    });
  };
  
  const focusPreviousEnabled = () => {
    focusManager.current?.focusPrevious({
      accept: (node) => {
        const index = parseInt(node.getAttribute('data-index') || '0');
        return !isDisabled(items[index]);
      }
    });
  };
  
  return (
    <div ref={containerRef}>
      <button onClick={focusPreviousEnabled}>Previous Enabled</button>
      <button onClick={focusNextEnabled}>Next Enabled</button>
      {items.map((item, index) => (
        <button
          key={index}
          data-index={index}
          disabled={isDisabled(item)}
          tabIndex={-1}
        >
          {item.name}
        </button>
      ))}
    </div>
  );
}

getFocusableTreeWalker Function

Creates a TreeWalker that matches all focusable or tabbable elements within a root element, with optional filtering.

/**
 * Create a TreeWalker that matches all focusable/tabbable elements.
 */
function getFocusableTreeWalker(
  root: Element,
  opts?: FocusManagerOptions,
  scope?: Element[]
): TreeWalker | ShadowTreeWalker;

Usage Examples:

import React, { useRef } from "react";
import { getFocusableTreeWalker } from "@react-aria/focus";

// Find all focusable elements
function FocusableElementFinder() {
  const containerRef = useRef<HTMLDivElement>(null);
  
  const findFocusableElements = () => {
    if (!containerRef.current) return [];
    
    const walker = getFocusableTreeWalker(containerRef.current, {
      tabbable: false // Include all focusable elements, not just tabbable ones
    });
    
    const elements: Element[] = [];
    let node = walker.nextNode() as Element;
    
    while (node) {
      elements.push(node);
      node = walker.nextNode() as Element;
    }
    
    return elements;
  };
  
  const logFocusableElements = () => {
    const elements = findFocusableElements();
    console.log('Focusable elements:', elements);
  };
  
  return (
    <div ref={containerRef}>
      <button onClick={logFocusableElements}>Find Focusable Elements</button>
      <input type="text" placeholder="Focusable input" />
      <button>Focusable button</button>
      <div tabIndex={0}>Focusable div</div>
      <a href="#" tabIndex={-1}>Non-tabbable link</a>
      <button disabled>Disabled button</button>
    </div>
  );
}

// Count tabbable elements
function TabbableCounter() {
  const containerRef = useRef<HTMLFormElement>(null);
  
  const countTabbableElements = () => {
    if (!containerRef.current) return 0;
    
    const walker = getFocusableTreeWalker(containerRef.current, {
      tabbable: true
    });
    
    let count = 0;
    while (walker.nextNode()) {
      count++;
    }
    
    return count;
  };
  
  return (
    <form ref={containerRef}>
      <p>Tabbable elements: {countTabbableElements()}</p>
      <input type="text" />
      <button type="button">Button</button>
      <select>
        <option>Option 1</option>
      </select>
      <textarea></textarea>
    </form>
  );
}

// Custom traversal with filtering
function FilteredTraversal() {
  const containerRef = useRef<HTMLDivElement>(null);
  
  const findButtonElements = () => {
    if (!containerRef.current) return [];
    
    const walker = getFocusableTreeWalker(containerRef.current, {
      tabbable: true,
      accept: (node) => node.tagName === 'BUTTON'
    });
    
    const buttons: Element[] = [];
    let node = walker.nextNode() as Element;
    
    while (node) {
      buttons.push(node);
      node = walker.nextNode() as Element;
    }
    
    return buttons;
  };
  
  return (
    <div ref={containerRef}>
      <input type="text" />
      <button>Button 1</button>
      <select><option>Select</option></select>
      <button>Button 2</button>
      <textarea></textarea>
      <button>Button 3</button>
      <p>Found {findButtonElements().length} buttons</p>
    </div>
  );
}

useHasTabbableChild Hook

Returns whether an element has a tabbable child and updates as children change.

/**
 * Returns whether an element has a tabbable child, and updates as children change.
 * @private - Internal utility for special cases
 */
function useHasTabbableChild(
  ref: RefObject<Element | null>,
  options?: AriaHasTabbableChildOptions
): boolean;

interface AriaHasTabbableChildOptions {
  isDisabled?: boolean;
}

Usage Examples:

import React, { useRef } from "react";
import { useHasTabbableChild } from "@react-aria/focus";

// Dynamic empty state handling
function CollectionContainer({ items, emptyMessage }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const hasTabbableChild = useHasTabbableChild(containerRef);
  
  // Show different tabIndex based on whether container has tabbable children
  const containerTabIndex = hasTabbableChild ? -1 : 0;
  
  return (
    <div 
      ref={containerRef} 
      tabIndex={containerTabIndex}
      role="grid"
      aria-label={items.length === 0 ? "Empty collection" : "Collection"}
    >
      {items.length > 0 ? (
        items.map((item, index) => (
          <button key={index} role="gridcell">
            {item.name}
          </button>
        ))
      ) : (
        <div role="gridcell">
          {emptyMessage}
          <button>Add Item</button>
        </div>
      )}
    </div>
  );
}

// Conditional keyboard navigation
function ConditionalNavigation({ isNavigationDisabled, children }) {
  const containerRef = useRef<HTMLDivElement>(null);
  const hasTabbableChild = useHasTabbableChild(containerRef, {
    isDisabled: isNavigationDisabled
  });
  
  return (
    <div 
      ref={containerRef}
      data-has-tabbable-child={hasTabbableChild}
      tabIndex={hasTabbableChild ? -1 : 0}
    >
      Navigation Status: {hasTabbableChild ? 'Has tabbable children' : 'No tabbable children'}
      {children}
    </div>
  );
}

Focus Navigation Patterns

Tree Walker Behavior

The getFocusableTreeWalker function creates a DOM TreeWalker that:

  • Traverses elements in document order
  • Filters based on focusability rules (CSS, disabled state, visibility)
  • Supports both focusable and tabbable element detection
  • Handles Shadow DOM traversal when available
  • Respects custom acceptance criteria

Focusable vs Tabbable

Focusable Elements:

  • Can receive focus via JavaScript (element.focus())
  • Includes elements with tabIndex="-1"
  • May not be reachable via Tab navigation

Tabbable Elements:

  • Subset of focusable elements
  • Reachable via Tab/Shift+Tab navigation
  • Have tabIndex >= 0 or are naturally tabbable (buttons, inputs, etc.)

Custom Navigation Patterns

Common patterns supported by these utilities:

  • Arrow Key Navigation: Grid, list, and menu navigation
  • Page Up/Down: Large list scrolling with focus management
  • Home/End Keys: Jump to first/last focusable element
  • Letter Navigation: Type-ahead search in lists
  • Roving TabIndex: Single tab stop with internal arrow key navigation

Performance Considerations

  • TreeWalkers are more efficient than querySelectorAll for large DOMs
  • Focus managers cache the root element reference
  • MutationObserver in useHasTabbableChild automatically updates on DOM changes
  • Use tabbable: true when possible to reduce the search space

Install with Tessl CLI

npx tessl i tessl/npm-react-aria--focus

docs

focus-navigation.md

focus-ring.md

focus-scope.md

index.md

virtual-focus.md

tile.json