FocusScope

The focus-confinement primitive. Moves focus in on mount, keeps Tab inside while trapped, and returns focus to where it was on unmount — the piece every modal overlay is built on.

Basic usage

Install the package:

pnpm add @design-schema/react @design-schema/tokens

Import the stylesheet once for the app, then the component:

import '@design-schema/tokens/calm-precise/css';
import '@design-schema/react/index.css';
import { FocusScope } from '@design-schema/react';

Examples

Confirm your changes

import { FocusScope, Button, Text } from '@design-schema/react';

<FocusScope>
  <>
    <Text>Confirm your changes</Text>
    <Button label="Cancel" />
    <Button label="Continue" />
  </>
</FocusScope>

Props

6 rows
PropTypeDefaultDescriptionPlatforms
children requiredcontent—

The confined content. The scope renders no element of its own beyond a wrapper that is not focusable and not announced.

web, lit, rn, swiftui
trappedbooleantrue

Tab and Shift+Tab wrap within the scope's focusable descendants, and focus that lands outside is pulled back in. False turns the scope into a plain "move focus in and restore on exit" helper, for non-modal panels.

web, lit, rn, swiftui
autoFocusfirst | last | container | nonefirst

Where focus goes on mount: the first focusable descendant, the last, the scope's own wrapper (made focusable with tabindex -1, for reading-first dialogs), or nowhere.

web, lit, rn, swiftui
restoreFocusbooleantrue

On unmount, focus returns to the element that was focused when the scope mounted, or to the next focusable element in the document if that one is gone.

web, lit, rn, swiftui
returnFocusToobject RefObject<HTMLElement | View>—

Explicit element to restore focus to instead of the recorded opener. Required on native when the opener is not a TextInput (React Native exposes no generic "currently focused element"), so every overlay passes its trigger ref. `shape` is a cross-platform sketch, never the literal type a platform emits; each types it with its own instance: `RefObject<HTMLElement | null>` on web, `HTMLElement | Ref<HTMLElement>` (lit/directives/ref.js) as a non-attribute property on Lit, `RefObject<ViewInstance | null>` on React Native.

web, lit, rn, swiftui
activebooleantrue

Pause the scope without unmounting it — used while a nested scope (a Menu inside a Dialog) is open, so the innermost active scope owns Tab.

web, lit, rn, swiftui

Events

1 row
EventTypeDescriptionPlatforms
onEscapeAttemptonEscapeAttempt / escape-attempt

Fired when trapped focus would have left the scope (Tab from the last element, Shift+Tab from the first) just before it wraps, with the direction. Diagnostic; components do not need it.

web, lit, rn, swiftui

Accessibility

No implicit ARIA role — the component is styling and structure, and adds nothing to the accessibility tree.

Anatomy

scope

What generation guarantees

Focus trap — While it is open, Tab cycles inside it; focus cannot reach the page behind.

Focus restore — On close, focus returns to whatever opened it.

Keyboard operable — Everything the pointer can do here, the keyboard can do — see the keyboard section.

FocusScope keyboard support

3 rows
KeyAction
TabFrom the last focusable descendant, wraps to the first.
Shift+TabFrom the first focusable descendant, wraps to the last.
TabOrdinary forward movement inside the scope; native focus order, no handler.

This component declares no contrast pair of its own — it inherits the colour of whatever it is placed in — and it does not animate.