focus-trap-tiny
v1.1.0
Published
A lightweight, zero-dependency focus trap for modals and dialogs. Uses native browser APIs.
Maintainers
Readme
focus-trap-tiny
A lightweight, zero-dependency focus trap for modals and dialogs.
Uses native browser APIs — no polyfills, no bloat.
Why?
Existing solutions like focus-trap are powerful but pull in dependencies and polyfills you probably don't need anymore. focus-trap-tiny does one thing well: traps keyboard focus inside a container using only what the browser already gives you.
- < 1 KB minified + gzipped
- Zero dependencies
- Works with any framework or vanilla JS
- Handles
Shift+Tab,inert, hidden elements, and dynamic content - Catches focus that escapes via mouse click or programmatic
focus(), not justTab - Configurable initial / return focus, with a fallback for empty dialogs
- Returns focus on deactivation (great for modals)
Install
npm install focus-trap-tinyUsage
import createFocusTrap from 'focus-trap-tiny';
const modal = document.getElementById('my-modal');
const trap = createFocusTrap(modal, {
onEscape: () => closeModal(),
});
// When modal opens
trap.activate();
// When modal closes
trap.deactivate();API
createFocusTrap(container, options?)
| Parameter | Type | Description |
|-------------|---------------|--------------------------------------|
| container | HTMLElement | The element to trap focus within. |
| options | Object | Optional configuration (see below). |
Options
| Option | Type | Default | Description |
|-----------------|-------------------------------|---------|-----------------------------------------------------------------------------|
| initialFocus | boolean \| FocusTarget | true | Where to send focus on activate(). true = first focusable, false = none, or a target. |
| returnFocus | boolean \| FocusTarget | true | Where to send focus on deactivate(). true = previously focused element, false = none, or a target. |
| fallbackFocus | FocusTarget | null | Element to focus when the container has no focusable children. Defaults to the container itself. |
| onEscape | Function | null | Callback fired when the Escape key is pressed. |
A
FocusTargetis a CSS selector string, anHTMLElement, or a function returning an element. ForinitialFocus/fallbackFocus, selectors resolve within the container; forreturnFocus, they resolve against the whole document (since the return target is usually the trigger outside the modal).
createFocusTrap(modal, {
initialFocus: '#email', // focus a specific field on open
returnFocus: triggerButton, // restore focus to the button that opened it
});Returns
| Method | Description |
|--------------|-----------------------------------------------------------|
| activate() | Starts the focus trap. |
| deactivate() | Stops the trap and optionally restores previous focus. |
| update() | Re-scans focusable elements (call after DOM changes). |
What counts as "focusable"?
<a href><button>(not disabled)<input>,<select>,<textarea>(not disabled)- Elements with
tabindex≥ 0 <details> > <summary><audio controls>,<video controls>
Elements that are display: none, visibility: hidden, or inside an [inert] container are automatically excluded. If the container has no focusable children, focus falls back to the container itself (or your fallbackFocus target) so focus is never lost.
Framework Examples
React
import { useEffect, useRef } from 'react';
import createFocusTrap from 'focus-trap-tiny';
function Modal({ isOpen, onClose, children }) {
const ref = useRef(null);
useEffect(() => {
if (!isOpen || !ref.current) return;
const trap = createFocusTrap(ref.current, { onEscape: onClose });
trap.activate();
return () => trap.deactivate();
}, [isOpen, onClose]);
if (!isOpen) return null;
return <div ref={ref} role="dialog" aria-modal="true">{children}</div>;
}Vue
<script setup>
import { ref, watch } from 'vue';
import createFocusTrap from 'focus-trap-tiny';
const props = defineProps(['isOpen']);
const emit = defineEmits(['close']);
const dialogRef = ref(null);
watch(() => props.isOpen, (open) => {
if (open && dialogRef.value) {
const trap = createFocusTrap(dialogRef.value, { onEscape: () => emit('close') });
trap.activate();
}
});
</script>License
MIT
