Skip to main content
FocusGuards injects a pair of invisible, focusable elements at the start and end of the document body. These “guards” ensure that focusin and focusout events can be captured consistently, which is essential for focus management in complex UIs.

Installation

npm install @radix-ui/react-focus-guards

Components

FocusGuards

A component that renders its children and ensures focus guards are present in the document.
interface FocusGuardsProps {
  children?: React.ReactNode;
}

Hook

useFocusGuards

A hook that ensures focus guards are injected without needing to render a component.
function useFocusGuards(): void

Usage

Basic Component Usage

import { FocusGuards } from '@radix-ui/react-focus-guards';
import { FocusScope } from '@radix-ui/react-focus-scope';

function Modal({ children }: { children: React.ReactNode }) {
  return (
    <FocusGuards>
      <FocusScope trapped>
        <div className="modal">
          {children}
        </div>
      </FocusScope>
    </FocusGuards>
  );
}

Using the Hook

import { useFocusGuards } from '@radix-ui/react-focus-guards';
import { FocusScope } from '@radix-ui/react-focus-scope';

function Dialog({ children }: { children: React.ReactNode }) {
  useFocusGuards();

  return (
    <FocusScope trapped>
      <div className="dialog">
        {children}
      </div>
    </FocusScope>
  );
}

App-level Setup

import { FocusGuards } from '@radix-ui/react-focus-guards';

function App() {
  return (
    <FocusGuards>
      <Router>
        <YourApp />
      </Router>
    </FocusGuards>
  );
}

With Multiple Modals

import { useFocusGuards } from '@radix-ui/react-focus-guards';
import { FocusScope } from '@radix-ui/react-focus-scope';

function ModalManager({ children }: { children: React.ReactNode }) {
  // Only one set of focus guards needed for multiple modals
  useFocusGuards();

  return <>{children}</>;
}

function App() {
  const [modal1Open, setModal1Open] = useState(false);
  const [modal2Open, setModal2Open] = useState(false);

  return (
    <ModalManager>
      <button onClick={() => setModal1Open(true)}>Open Modal 1</button>
      <button onClick={() => setModal2Open(true)}>Open Modal 2</button>
      
      {modal1Open && (
        <FocusScope trapped>
          <div>Modal 1 Content</div>
        </FocusScope>
      )}
      
      {modal2Open && (
        <FocusScope trapped>
          <div>Modal 2 Content</div>
        </FocusScope>
      )}
    </ModalManager>
  );
}

Conditional Focus Management

import { useFocusGuards } from '@radix-ui/react-focus-guards';

function ConditionalDialog({ isOpen, children }: { 
  isOpen: boolean; 
  children: React.ReactNode;
}) {
  // Guards only injected when component is mounted
  useFocusGuards();

  if (!isOpen) return null;

  return (
    <div className="dialog">
      {children}
    </div>
  );
}

How It Works

Focus guards are invisible <span> elements with these properties:
  • data-radix-focus-guard attribute for identification
  • tabIndex={0} to make them focusable
  • Positioned fixed with zero opacity (invisible but functional)
  • No pointer events
  • Inserted at document.body edges (afterbegin and beforeend)
The guards ensure:
  • Focus events bubble correctly through the document
  • Focus trapping can detect when focus leaves the trapped area
  • Tab navigation at document boundaries works consistently

Reference Counting

The implementation uses reference counting:
  • Each call to useFocusGuards() increments an internal counter
  • Guards are only removed when the counter reaches zero
  • Multiple components can safely use focus guards simultaneously
  • Guards are shared across all instances (only one pair exists)

Implementation Details

function createFocusGuard() {
  const element = document.createElement('span');
  element.setAttribute('data-radix-focus-guard', '');
  element.tabIndex = 0;
  element.style.outline = 'none';
  element.style.opacity = '0';
  element.style.position = 'fixed';
  element.style.pointerEvents = 'none';
  return element;
}
The hook:
  1. Checks for existing guards on mount
  2. Creates new guards if none exist
  3. Inserts them at document body edges
  4. Increments the reference counter
  5. Decrements counter and removes guards on unmount (if counter reaches 0)

When to Use

Use focus guards when:
  • Implementing focus trapping (modals, dialogs)
  • Building focus management utilities
  • Working with FocusScope or similar components
  • Need reliable focus event capture across the document

Notes

Focus guards are automatically shared across all components that use them. You don’t need to worry about duplicate guards - the implementation ensures only one pair exists in the document.
The guards are completely invisible and non-interactive to users. They exist purely to ensure browser focus events work correctly for JavaScript-based focus management.
If you’re using FocusScope or other Radix UI components that handle focus, you typically need focus guards. Many Radix primitives handle this automatically.
The reference counting ensures guards remain in the document as long as any component needs them, and are only removed when no components require them anymore.

Build docs developers (and LLMs) love