Skip to content

Headless Core — Modal

Powers Modal and and-vanilla-modal internally. Note what this factory doesn’t do: focus trapping and body-scroll locking are real DOM operations, so they live in the consuming layer (@andersseen/web-components’s utils/focus-trap.ts) — this factory only tracks isOpen and hands back the ARIA props and event-handler logic.

import { createModal } from '@andersseen/headless-components/modal';
const modal = createModal({
closeOnEscape: true,
closeOnOverlayClick: true,
onOpenChange: isOpen => console.log('Modal:', isOpen),
});
modal.subscribe(state => {
if (state.isOpen) console.log('Modal opened');
});
modal.actions.open();
window.addEventListener('keydown', modal.handleKeyDown);
OptionTypeDefaultNotes
defaultOpenbooleanfalse
onOpenChange(isOpen: boolean) => void
closeOnEscapebooleantrue
closeOnOverlayClickbooleantrue
disabledbooleanfalseBlocks open(); setting true while open closes it.
labelstring'Dialog'Used as aria-label on the content element.

isOpen, disabled.

open(), close(), toggle(), setDisabled(v) — no queries bucket on this factory (unlike most others); read modal.state.isOpen directly.

GetterReturns
getOverlayProps()data-state, aria-hidden
getContentProps()role: 'dialog', aria-modal: true, aria-hidden, aria-label, data-state, tabindex: -1
getCloseButtonProps()aria-label: 'Close', type: 'button'

handleKeyDown(event) closes on Escape when closeOnEscape and the modal is open — wire it to window, not the content element, since a modal’s content is often not focused. handleOverlayClick() closes when closeOnOverlayClick; call it from the overlay’s click handler.

Toggle modal

Drawer is the same open/close/Escape/overlay shape with an added placement side.