focus-trap-vue

A Vue 3 component that traps keyboard focus inside modals, dialogs, and other overlay UI for accessible interfaces.

Library
npm
v4.1.0
216stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
65/100Good
Development Activity80
Maintenance56
Community44
Maturity60
Momentum20

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
77/100Good
Architecture75
Code Quality78
Innovation85
Learning Curve70

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 FocusTrap component with a near 1:1 typed prop mapping onto focus-trap’s own Options API
  • v-model:active binding for declarative activate/deactivate control, plus imperative activate()/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@legacy package 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.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search