Action

Focus Trap

Keeps focus within an element for accessibility.

/// DEMO ///

Press tab key to cycle through focusable elements.

Email

Password

///

1
<script>
2
  import Button from '$lib/components/base/button.svelte';
3
  import Field from '$lib/components/base/field.svelte';
4
  import Input from '$lib/components/base/input.svelte';
5
  import { focusTrap } from '$lib/actions/focus-trap.svelte';
6
</script>
7

8
<!-- Use focusTrap to keep focus within the form -->
9
<form use:focusTrap class="flex flex-col gap-2 border p-4">
10
  <p>Press tab key to cycle through focusable elements.</p>
11
  <Field label="Email">
12
    <Input type="email" />
13
  </Field>
14
  <Field label="Password">
15
    <Input type="password" />
16
  </Field>
17
  <Field class="mt-4 flex justify-end">
18
    <Button>Sign In</Button>
19
  </Field>
20
</form>
21

Guide

Create the action.

1
export function focusTrap(node: HTMLElement) {
2
  const focusableSelectors = ['a[href]', 'button', 'input', 'textarea', 'select', '[tabindex]:not([tabindex="-1"])'];
3

4
  function getFocusableElements() {
5
    return Array.from(node.querySelectorAll<HTMLElement>(focusableSelectors.join(','))).filter(
6
      (el) => !el.hasAttribute('disabled') && !el.getAttribute('aria-hidden')
7
    );
8
  }
9

10
  function handleKeyDown(event: KeyboardEvent) {
11
    if (event.key !== 'Tab') return;
12

13
    const focusableElements = getFocusableElements();
14
    const firstElement = focusableElements[0];
15
    const lastElement = focusableElements[focusableElements.length - 1];
16

17
    if (event.shiftKey && document.activeElement === firstElement) {
18
      event.preventDefault();
19
      lastElement?.focus();
20
    } else if (!event.shiftKey && document.activeElement === lastElement) {
21
      event.preventDefault();
22
      firstElement?.focus();
23
    }
24
  }
25

26
  $effect(() => {
27
    const focusableElements = getFocusableElements();
28
    focusableElements[0]?.focus();
29

30
    node.addEventListener('keydown', handleKeyDown);
31

32
    return () => {
33
      node.removeEventListener('keydown', handleKeyDown);
34
    };
35
  });
36
}
37

Share treats:

Copyright © 2025 - Cliemtor Fabros