focus-trap-vue
A Vue 3 component that traps keyboard focus inside modals, dialogs, and other overlay UI for accessible interfaces.
Repository Health
Technical Analysis
focus-trap-vue wraps the framework-agnostic focus-trap library in a single Vue component so modals, dialogs, and other overlays keep keyboard focus contained without any imperative wiring. Its props mirror focus-trap’s own options almost directly, so escape-to-close behavior, outside-click handling, and initial/fallback focus targets are all configurable declaratively.
The component is driven primarily through v-model:active, wrapping exactly one child element or component and activating/deactivating the underlying trap as that binding changes — including correctly tearing down and recreating the trap when the wrapped node is removed via v-if. A legacy focus-trap-vue@legacy build is maintained separately for Vue 2 consumers.
What You Get
- A single
FocusTrapcomponent with a near 1:1 typed prop mapping onto focus-trap’s ownOptionsAPI v-model:activebinding for declarative activate/deactivate control, plus imperativeactivate()/deactivate()methods- Automatic teardown and recreation of the trap when the wrapped element is removed and reinserted via
v-if - Multiple published build targets (CJS, ESM bundler, ESM browser, UMD/global) plus a rolled-up
.d.ts - A separate
focus-trap-vue@legacypackage build for Vue 2 projects
Common Use Cases
- Trapping focus inside a modal dialog so Tab/Shift+Tab cycle only through its contents
- Building accessible dropdown menus, popovers, or off-canvas panels that shouldn’t leak focus to the page behind them
- Returning focus to a triggering element once an overlay closes
- Combining with a UI component library’s own modal/dialog primitive when that library doesn’t handle focus trapping itself
Under The Hood
Architecture
FocusTrap.ts defines a single Vue component using the Composition API: a computed el resolves the wrapped node to either a raw HTMLElement or a component instance’s $el, ensureTrap() lazily constructs the underlying focus-trap instance the first time it’s needed, and a post-flush watch on the active prop drives activation/deactivation — nulling out the trap reference when the wrapped node is removed so v-if blocks correctly force a fresh trap on re-insertion. The custom render() function filters out comment vnodes and clones the single default-slot child with a ref callback, since the component needs a real DOM/component reference to attach the trap to rather than rendering its own wrapper element.
Tech Stack
The only runtime dependency is a peer dependency on focus-trap (^7.0.0) alongside vue (^3.0.0); everything else is dev tooling. Rollup with @rollup/plugin-* plugins builds the library into CJS, ESM-bundler, ESM-browser, and global/UMD outputs (declared via package.json’s main/module/unpkg/jsdelivr fields), @microsoft/api-extractor rolls the TypeScript types into a single .d.ts, Vite serves the demo app, Cypress drives end-to-end tests against that demo, and size-limit enforces a bundle-size budget in CI.
Code Quality
There are no component-level unit tests; correctness is verified entirely through Cypress end-to-end tests that drive a live demo app through several trap configurations (escape-to-close, outside click, nested Vue components) and assert on DOM state. The source is TypeScript compiled under strict: true with noImplicitAny and noUnusedLocals, checked via tsc --build, and Prettier is the sole formatter/linter (pnpm lint). GitHub Actions workflows run the Cypress suite, the size-limit check, and release tagging on every change.
API Design
The library exposes exactly one named export, FocusTrap, whose props are generated through a typed defineFocusTrapProps helper that maps directly onto focus-trap’s own Options type — so anyone already familiar with focus-trap has almost nothing new to learn. The primary usage pattern, v-model:active wrapping a single child, enforces (and warns on) the exactly-one-child constraint at render time, while activate/deactivate are also exposed as callable methods for components that need imperative control instead of the reactive binding.