npm package: @rightxt/focus-trap-vue
Description: This library provides a Vue-component wrapper around 'focus-trap'.
Author: Igor Iakovlev
Latest version: 0.9.0

# @rightxt/focus-trap-vue

[![License](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/rightxt/focus-trap-vue/blob/main/LICENSE)
[![NPM version](https://img.shields.io/npm/v/@rightxt/focus-trap-vue)](https://www.npmjs.com/package/@rightxt/focus-trap-vue)

This library provides a Vue-component wrapper around 'focus-trap'.

Before getting started, it is recommended to read the [documentation](https://github.com/focus-trap/focus-trap) to understand what a 'focus-trap' is.

## Requirements

- Vue: ^3.5
- focus-trap: ^7.6.5
- Node: >=18

The library never bundles 'vue' and 'focus-trap'.

## Installation

```bash
# npm
npm i @rightxt/focus-trap-vue

# pnpm
pnpm add @rightxt/focus-trap-vue

# yarn
yarn add @rightxt/focus-trap-vue
```

## Build variants

The package publishes several artifacts.

| Variant      | Import/Include                                                         | Suitable for             | You must provide                                     |
|--------------|------------------------------------------------------------------------|--------------------------|------------------------------------------------------|
| **Core ESM** | `import plugin from '@rightxt/focus-trap-vue'`                         | Vite / Webpack / Rollup  | Install 'focus-trap' (peer)                          |
| **IIFE**     | `<script src="rxt-focus-trap-vue.iife.js">` → `window.RxtFocusTrapVue` | Static pages w/o bundler | Load before: UMD 'tabbable', UMD 'focus-trap', 'vue' |


As a plugin:

```js
import { createApp } from 'vue';
import Plugin from '@rightxt/focus-trap-vue';
import App from './App.vue';

const app = createApp(App);
app.use(Plugin);
app.mount('#app');
```

Component registration:

```js
import { createApp } from 'vue';
import { FocusTrap, FocusTrapExclude } from '@rightxt/focus-trap-vue';
import App from './App.vue';

const app = createApp(App);
app.component('FocusTrap', FocusTrap);
app.component('FocusTrapExclude', FocusTrapExclude);
app.mount('#app');
```

## API

Package exports

default:

- plugin `{ install(app) }`

Named:

- `FocusTrap` — the main trap component
- `FocusTrapExclude` — excludes a node/subtree from inertness
- TypeScript types

```ts
import type {
  ActivateOptions,
  CreateOptions,
  CreateOptionsProps,
  DeactivateOptions,
  ExclusionsApi,
  FocusableElement,
  FocusTarget,
  FocusTargetOrFalse,
  FocusTargetValueOrFalse,
  FocusTrapI,
  FocusTrapState,
  FocusTrapTabbableOptions,
  InertEntry,
  KeyboardEventToBoolean,
  MouseEventToBoolean,
  PauseOptions,
  PropsOptions,
  RevertRecord,
  UnpauseOptions,
  VoidFunction,
  FocusTrapVue,
} from '@rightxt/focus-trap-vue';
import Plugin, { FocusTrap, FocusTrapExclude } from '@rightxt/focus-trap-vue';
```

## Library components

### **FocusTrap**

The component's content will be wrapped with 'focus-trap'.

| Prop             | Type            | Default | Description                                                                                                                            |
|------------------|-----------------|---------|----------------------------------------------------------------------------------------------------------------------------------------|
| `v-model:active` | `boolean`       | `false` | Activates/deactivates the trap                                                                                                         |
| `v-model:paused` | `boolean`       | `false` | Pauses/unpauses the trap                                                                                                               |
| `createOptions`  | `CreateOptions` | `null`  | Options proxied to 'focus-trap' ([focus-trap](https://github.com/focus-trap/focus-trap?tab=readme-ov-file#trap-configuration-options)) |
| `renderless`     | `boolean`       | `false` | If `true`, does not create a wrapper and wraps exactly one child                                                                       |
| `tag`            | `string`        | `'div'` | Root element tag when `renderless = false`                                                                                             |

Slots:

- `default` — content within which focus is trapped

Events:

- `update:active(boolean)` — trap activation state change
- `update:paused(boolean)` — trap pause state change
- `created` — component created
- `destroyed` — component destroyed
- `error(where, error?)` — an exception occurred inside the component
- `inner(handlerName)` — events re-emitting internal 'focus-trap' events

Expose methods:

- `activate(options?)` — activate the trap ([focus-trap](https://github.com/focus-trap/focus-trap?tab=readme-ov-file#trapactivate))
- `deactivate(options?)` — deactivate the trap ([focus-trap](https://github.com/focus-trap/focus-trap?tab=readme-ov-file#trapdeactivate))
- `pause(options?)` — pause an active trap ([focus-trap](https://github.com/focus-trap/focus-trap?tab=readme-ov-file#trappause))
- `unpause(options?)` — unpause an active trap ([focus-trap](https://github.com/focus-trap/focus-trap?tab=readme-ov-file#trapunpause))
- `update()` — force the component's DOM to update and reinitialize the trap
- `getState()` — get the internal 'focus-trap' state as an object with two properties: `{ active, paused }`
- `rootElement` — a ref to the trap's root DOM element

Example:

```
<script setup lang="ts">
import { ref } from 'vue';
import { FocusTrap } from '@rightxt/focus-trap-vue';
const active = ref<boolean>(false);
const paused = ref<boolean>(false);
</script>

<template>
  <FocusTrap v-model:active="active" v-model:paused="paused">...</FocusTrap>
</template>
```

### **FocusTrapExclude**

Marks a node/subtree inside the trap that should not become inert while the trap is active.

This component is only used inside **FocusTrap**!

Example:

```
<script setup lang="ts">
import { ref } from 'vue';
import { FocusTrap, FocusTrapExclude } from '@rightxt/focus-trap-vue';
const active = ref<boolean>(false);
const paused = ref<boolean>(false);
</script>

<template>
  <FocusTrap v-model:active="active" v-model:paused="paused">
    ...
    <FocusTrapExclude>
      <p>Here is <a href="#">something</a> (excluded group).</p>
      <p>Here is <a href="#">something</a> (excluded group).</p>
    </FocusTrapExclude>
    ...
  </FocusTrap>
</template>
```

## Demo

- [ESM](https://rightxt.github.io/focus-trap-vue/)
- [IIFE](https://rightxt.github.io/focus-trap-vue/iife.html)

## FAQ

> Can I use it without the plugin?

Yes. Import it as a component.

> Why does `v-model:paused` sometimes revert to `true`?

If `unpause()` didn't take effect (the trap is not at the top of the stack), the component resyncs the model back so it reflects the trap's actual state. This matches 'focus-trap' behavior.

> Why isn't the library a lightweight wrapper around 'focus-trap' like some alternatives, and what are its key features?

Because the library aims to ease developer experience by providing extra functionality for convenience.

The **FocusTrap** component creates an additional root HTML element by default. This ensures the trap works correctly even when there are temporarily no focusable elements inside — you don't have to track this manually.

If you set `renderless=true`, the component does not create a wrapper and wraps exactly one child. In this mode, the responsibility for having a proper container and focusable elements is on the developer.

You can control the trap declaratively (`v-model:active`, `v-model:paused`) or imperatively via expose methods (`activate`, `deactivate`, `pause`, `unpause`).

The additional **FocusTrapExclude** component lets you exclude specific elements or subtrees from the 'inertness' mode when the trap is active.