npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fineanmol/focus-trap-vue

v1.2.0

Published

Zero-dependency Vue 3 component that traps keyboard focus within a DOM element — written from scratch.

Readme

@fineanmol/focus-trap-vue

npm Demo

Vue 3 component to trap keyboard focus within a DOM element.

Written from scratch — no focus-trap peer dependency. Useful for modals, dialogs, drawers, and anything that needs to be keyboard-accessible.

→ Live demo

Installation

npm install @fineanmol/focus-trap-vue

Usage

FocusTrap can be controlled in three ways:

  • using the active prop directly
  • using v-model:active (recommended)
  • calling activate() / deactivate() on a template ref

v-model:active

<script setup>
import { ref } from 'vue'
import { FocusTrap } from '@fineanmol/focus-trap-vue'

const isOpen = ref(false)
</script>

<template>
  <button @click="isOpen = true">Open dialog</button>

  <FocusTrap v-model:active="isOpen">
    <dialog :open="isOpen">
      <h2>Trapped!</h2>
      <p>Tab stays inside while this is open.</p>
      <button @click="isOpen = false">Close</button>
    </dialog>
  </FocusTrap>
</template>

When isOpen becomes true, the trap activates and focus moves to the first tabbable element inside the dialog. Pressing Escape (or clicking "Close") sets isOpen back to false.

Imperative via template ref

<script setup>
import { ref } from 'vue'
import { FocusTrap } from '@fineanmol/focus-trap-vue'
import type { FocusTrapExposed } from '@fineanmol/focus-trap-vue'

const trap = ref<FocusTrapExposed>()
</script>

<template>
  <FocusTrap ref="trap" :active="false">
    <div role="dialog">
      <button @click="trap?.deactivate()">Close</button>
    </div>
  </FocusTrap>

  <button @click="trap?.activate()">Open</button>
</template>

Global registration

import { createApp } from 'vue'
import { FocusTrap } from '@fineanmol/focus-trap-vue'
import App from './App.vue'

createApp(App)
  .component('FocusTrap', FocusTrap)
  .mount('#app')

Props

FocusTrap requires exactly one child element (or component). It clones the child and attaches listeners to it.

| Prop | Type | Default | Description | |------|------|---------|-------------| | active | boolean | true | Whether the trap is on. Use v-model:active to sync with parent state. | | escapeDeactivates | boolean | true | Press Escape to close and restore focus. | | returnFocusOnDeactivate | boolean | true | When deactivating, return focus to the element that had it before activation. | | allowOutsideClick | boolean \| (e: MouseEvent\|TouchEvent) => boolean | true | Whether clicks outside the trap are allowed. Pass a function for per-click control. | | clickOutsideDeactivates | boolean \| (e: MouseEvent\|TouchEvent) => boolean | false | Close the trap when clicking outside. | | initialFocus | string \| HTMLElement \| () => HTMLElement \| false | first tabbable | What gets focused on activation. A CSS selector, an element, a function that returns one, or false to skip auto-focus. | | fallbackFocus | string \| HTMLElement \| () => HTMLElement | container | What to focus when no tabbable elements are found. Falls back to focusing the container itself if not set. | | delayInitialFocus | boolean | true | Wait a microtask before setting initial focus. Helpful when the child element has an enter animation. | | preventScroll | boolean | false | Passed to .focus({ preventScroll }) — stops the page from jumping when focusing an off-screen element. |

Events

| Event | Payload | When it fires | |-------|---------|---------------| | activate | — | The moment the trap turns on | | postActivate | — | After the initial element has been focused | | deactivate | — | The moment the trap turns off | | postDeactivate | — | After focus has been returned to the previous element | | update:active | boolean | For v-model:active two-way binding |

Methods (via template ref)

Use ref typed as FocusTrapExposed to get access to the imperative API:

import type { FocusTrapExposed } from '@fineanmol/focus-trap-vue'

const trap = ref<FocusTrapExposed>()

trap.value?.activate()    // turn the trap on
trap.value?.deactivate()  // turn it off and restore focus
trap.value?.pause()       // suspend trapping without deactivating
trap.value?.unpause()     // resume after a pause

Examples

Custom initial focus

<FocusTrap v-model:active="open" :initial-focus="() => nameInput">
  <dialog :open="open">
    <label>
      Name
      <input ref="nameInput" type="text" />
    </label>
    <button @click="open = false">Submit</button>
  </dialog>
</FocusTrap>

Skip auto-focus (just trap Tab)

<FocusTrap v-model:active="open" :initial-focus="false">
  <div role="dialog">...</div>
</FocusTrap>

Click outside closes the trap

<FocusTrap v-model:active="open" :click-outside-deactivates="true">
  <div class="dropdown">...</div>
</FocusTrap>

Selectively block outside clicks

<FocusTrap
  v-model:active="open"
  :allow-outside-click="(e) => e.target.closest('.toolbar') !== null"
>
  <div role="dialog">...</div>
</FocusTrap>

No Escape key

<FocusTrap v-model:active="open" :escape-deactivates="false">
  <div role="dialog">
    <button @click="open = false">Only way out</button>
  </div>
</FocusTrap>

Pause and unpause (nested traps)

If a second modal or tooltip opens on top of an existing trap, pause the outer one while the inner is active:

<script setup>
const outer = ref<FocusTrapExposed>()
const innerOpen = ref(false)

function openInner() {
  outer.value?.pause()
  innerOpen.value = true
}

function closeInner() {
  innerOpen.value = false
  outer.value?.unpause()
}
</script>

<template>
  <FocusTrap ref="outer" v-model:active="outerOpen">
    <div role="dialog">
      <button @click="openInner">Open inner</button>
    </div>
  </FocusTrap>

  <FocusTrap v-model:active="innerOpen" @deactivate="closeInner">
    <div role="dialog">...</div>
  </FocusTrap>
</template>

Fallback focus (no tabbable children)

<FocusTrap v-model:active="open" fallback-focus="#my-dialog">
  <div id="my-dialog" role="dialog">
    <!-- no interactive elements here, but focus will land on the div -->
    <p>Read-only content</p>
  </div>
</FocusTrap>

Utilities

The package exports its tabbable-element helpers if you need them directly:

import { getTabbable, getFirstTabbable, getLastTabbable } from '@fineanmol/focus-trap-vue'

const all     = getTabbable(containerEl)     // all focusable elements in order
const first   = getFirstTabbable(containerEl)
const last    = getLastTabbable(containerEl)

Elements are included only if they pass a visibility check (not display:none, visibility:hidden, hidden, or [inert]).

How it works

  • Tabbable detection — own selector-based scan covering <a>, <button>, <input>, <select>, <textarea>, [contenteditable], [tabindex], <details summary>, <audio controls>, <video controls>. Filtered for visibility and [inert] ancestors.
  • Tab cyclingkeydown listener attached in capture phase redirects Tab/Shift+Tab at the boundary, wrapping around.
  • Focus escape guardfocusin listener in capture phase catches focus landing outside the container and pulls it back.
  • Click outsidemousedown/touchstart captured before the click, checked against the container. Runs allowOutsideClick or clickOutsideDeactivates logic before deciding what to do.
  • Pause — sets an isPaused flag; all handlers skip processing while paused. The listeners stay attached so unpause is instant.
  • Cleanup — all listeners removed on deactivate() and also on onBeforeUnmount.

Dist formats

| File | Format | Use case | |------|--------|---------| | dist/focus-trap-vue.esm.js | ESM | Bundlers (Vite, webpack) | | dist/focus-trap-vue.cjs.js | CJS | Node / require() | | dist/focus-trap-vue.cjs.prod.js | CJS minified | Production Node | | dist/focus-trap-vue.global.js | IIFE | <script> tag / CDN |

License

MIT © Anmol Agarwal