useFocusTrap Composable
Accessibility-focused focus trap for modals and dialogs with keyboard navigation
Code
import { ref, onMounted, onUnmounted, type Ref } from 'vue';
export function useFocusTrap(
container: Ref<HTMLElement | null>,
active: Ref<boolean> = ref(true)
) {
const focusableSelectors = [
'button:not([disabled])',
'a[href]:not([disabled])',
'input:not([disabled]):not([type="hidden"])',
'select:not([disabled])',
'textarea:not([disabled])',
'[tabindex]:not([tabindex="-1"]):not([disabled])',
'[contenteditable="true"]:not([disabled])'
].join(',');
let focusableElements: HTMLElement[] = [];
let firstElement: HTMLElement | null = null;
let lastElement: HTMLElement | null = null;
let lastFocusedElement: HTMLElement | null = null;
// Get focusable elements
const getFocusableElements = () => {
__TOKEN_48__ (!container.value) return [];
return Array.__TOKEN_51__(container.value.querySelectorAll(focusableSelectors)) as HTMLElement[];
};
// Trap focus inside container
const trapFocus = (e: KeyboardEvent) => {
__TOKEN_53__ (!active.value || !container.value) return;
// Only handle Tab key
__TOKEN_55__ (e.key !== 'Tab') return;
focusableElements = getFocusableElements();
__TOKEN_57__ (focusableElements.length === 0) return;
firstElement = focusableElements[0];
lastElement = focusableElements[focusableElements.length - 1];
const currentFocused = document.activeElement as HTMLElement;
// Reverse tab (Shift + Tab)
__TOKEN_60__ (e.shiftKey && currentFocused === firstElement) {
e.preventDefault();
lastElement?.focus();
}
// Forward tab
else __TOKEN_62__ (!e.shiftKey && currentFocused === lastElement) {
e.preventDefault();
firstElement?.focus();
}
};
// Activate focus trap
const activate = () => {
__TOKEN_64__ (!container.value) return;
// Save last focused element
lastFocusedElement = document.activeElement as HTMLElement;
// Get focusable elements and focus first
focusableElements = getFocusableElements();
__TOKEN_66__ (focusableElements.length > 0) {
focusableElements[0].focus();
}
// Add event listener
document.addEventListener('keydown', trapFocus);
};
// Deactivate focus trap
const deactivate = () => {
document.removeEventListener('keydown', trapFocus);
// Restore focus to last focused element
__TOKEN_68__ (lastFocusedElement) {
lastFocusedElement.focus();
}
};
// Watch active state
const updateFocusTrap = () => {
__TOKEN_70__ (active.value) {
activate();
} else {
deactivate();
}
};
onMounted(() => {
__TOKEN_72__ (active.value) {
activate();
}
// Watch for active state changes (if reactive)
__TOKEN_73__ (typeof active === 'object' && 'value' in active) {
const stopWatch = watch(active, updateFocusTrap);
onUnmounted(stopWatch);
}
});
onUnmounted(() => {
deactivate();
});
return { activate, deactivate };
}
// Usage example
// const modalRef = ref<HTMLElement | null>(null);
// const isModalOpen = ref(false);
// const { activate, deactivate } = useFocusTrap(modalRef, isModalOpen);
// // Open modal
// isModalOpen.value = true;
// activate();
// // Close modal
// isModalOpen.value = false;
// deactivate();