Hooks

useFocusTrap

Trap keyboard focus within a container while active. Tab and Shift+Tab cycle through the focusable descendants only. Focuses an initial element on activation, and by default restores focus to the previously focused element on deactivation or unmount. SSR-safe.

Note: Components built on bits-ui primitives (Modal, Slideover, Drawer, Popover) already manage focus internally - do not wrap them with useFocusTrap.

Basic usage

Bind the container with bind:this, pass a reactive getter for active. When open is true, Tab cycles only among the elements inside the panel.

<script lang="ts">
  import { useFocusTrap } from 'sv5ui';

  let open = $state(false);
  let panel = $state<HTMLDivElement>();

  useFocusTrap(() => panel, { active: () => open });
</script>

<button onclick={() => (open = true)}>Open panel</button>

{#if open}
  <div bind:this={panel} class="rounded border p-4">
    <p>Focus is trapped here. Tab cycles through the buttons below.</p>
    <input placeholder="First focusable" class="block rounded border px-2 py-1" />
    <input placeholder="Second focusable" class="block rounded border px-2 py-1 mt-2" />
    <button onclick={() => (open = false)} class="mt-2">Close</button>
  </div>
{/if}

Custom initial focus

Use initialFocus to point focus at a specific element (e.g. a close button) instead of the first focusable element. The getter is resolved at activation time.

<script lang="ts">
  import { useFocusTrap } from 'sv5ui';

  let open = $state(false);
  let panel = $state<HTMLDivElement>();
  let closeBtn = $state<HTMLButtonElement>();

  useFocusTrap(() => panel, {
    active: () => open,
    // Move focus to the close button on open instead of the first focusable
    initialFocus: () => closeBtn ?? null,
    // Return focus to the trigger on close (default: true)
    restoreFocus: true
  });
</script>

<button onclick={() => (open = true)}>Open panel</button>

{#if open}
  <div bind:this={panel} class="rounded border p-4">
    <input placeholder="Not focused first" class="block rounded border px-2 py-1" />
    <button bind:this={closeBtn} onclick={() => (open = false)} class="mt-2">
      Close (gets initial focus)
    </button>
  </div>
{/if}

Parameters

ParameterTypeDefault
targetHTMLElement | (() => HTMLElement | null) | null-

Options

OptionTypeDefault
activeboolean | (() => boolean)true
initialFocusHTMLElement | (() => HTMLElement | null) | falsefirst focusable
restoreFocusbooleantrue