Skip to main content

Focus Trap

BetaSince 0.1.0

Keeps keyboard focus inside its container, for modal content such as Dialog.

import { FocusTrap } from "@anantya/uix";

Overview

FocusTrap keeps keyboard focus inside its container while it is active, so Tab and Shift+Tab can't reach the page behind it. UIX components use it for modal content, such as Dialog.

Building block

For a modal window, use Dialog, or Drawer, coming in v1, for a side panel. Both trap focus already.

Missing a component? Ask for it instead of building your own.

Signature

function FocusTrap(props: FocusTrapProps): ReactElement;

interface FocusTrapProps extends HTMLAttributes<HTMLElement> {
active?: boolean;
asChild?: boolean;
initialFocus?: RefObject<HTMLElement | null>;
loop?: boolean;
restoreFocus?: boolean;
ref?: Ref<HTMLElement>;
}

Parameters

PropTypeDefaultDescription
activebooleantrueKeeps focus inside while true.
asChildbooleanfalseUses its one child element as the container, such as an <aside>, instead of a <div>.
initialFocusRefObject<HTMLElement | null>-The element to focus when it activates.
loopbooleantrueTab on the last element moves to the first, and Shift+Tab on the first to the last. When false, focus stops at both ends.
restoreFocusbooleantrueReturns focus, when it deactivates, to the element that had it before.
refRef<HTMLElement>-Gets the container.
Any HTML attributeHTMLAttributes<HTMLElement>-Passed to the container, such as role and aria-labelledby.

Returns

A <div tabindex="-1">, or its child with asChild, around its children. While it is active:

  • It focuses initialFocus first, else the first element with data-autofocus, else the element already focused inside, else the first one Tab reaches, else the container
  • Tab and Shift+Tab move through the elements inside, found again on every press, so ones added later count
  • Focus that lands outside, such as from a click, is pulled back inside
  • When the focused element is removed, the container takes focus

When it deactivates or unmounts, focus returns to the element that had it before. When one FocusTrap is inside another, the inner one is in control.

Example

Turn on Keep focus here, then press Tab: focus moves through the switch and the three buttons and wraps at the ends. Turn it off to let focus leave.

Code
"use client";

import { Button, FocusTrap, Switch } from "@anantya/uix";
import { useState } from "react";

export default function InvoiceActions() {
const [active, setActive] = useState(false);
return (
<FocusTrap
active={active}
className="flex flex-col items-center gap-4 rounded-xl border border-border p-4 text-sm"
>
<label className="flex items-center gap-3">
<Switch checked={active} onCheckedChange={setActive} />
Keep focus here
</label>
<div className="flex flex-wrap justify-center gap-2">
<Button variant="secondary">Download PDF</Button>
<Button variant="secondary">Send reminder</Button>
<Button>Mark as paid</Button>
</div>
</FocusTrap>
);
}

Notes

  • FocusTrap only moves keyboard focus. A modal also needs a role, a name, the rest of the page made inert and a way to close it with Esc, which Dialog has
  • Content rendered inside it through a Portal counts as inside, so a menu opened from inside can take focus
  • It does nothing on the server; it activates once the page is hydrated

Changelog

Every change to Focus Trap since 0.1.0 is in the release notes.

© Anantya TechLabs. All rights reserved.