Headless primitives
Headless primitives
@kolirt/vue-modal ships zero CSS for modal appearance. There is no default backdrop color, no panel size, no shadow, no transition. What you see is entirely what you write.
This is intentional. Modals live at the top of the visual hierarchy and need to match your design system exactly. Any "sensible default" would just need to be overridden.
Layout resets that are applied
A minimal layout reset is applied via :where() (specificity 0), so your CSS always wins without !important:
:where([data-modal-region]) {
position: fixed;
inset: 0;
pointer-events: none;
}
:where([data-modal-root]) {
position: absolute;
inset: 0;
pointer-events: auto;
}
:where([data-modal-overlay]) {
position: absolute;
inset: 0;
pointer-events: none;
}
:where([data-modal-content][data-instant][data-state='open']),
:where([data-modal-overlay][data-instant][data-state='open']) {
animation: none !important;
transition: none !important;
}
The !important rule applies only when instantEnter: true was passed. It is scoped to [data-state="open"], so it suppresses the enter animation only — your exit animation runs normally when the modal closes.
Built-in behavior
You don't configure focus or accessibility — they ship out of the box:
- Focus trap — keyboard focus is locked inside the modal while it is open.
- Focus return — focus returns to the trigger element when the modal closes.
- ARIA —
role="dialog",aria-modal,aria-labelledby/aria-describedbywired through<ModalTitle>and<ModalDescription>. - Exit-animation wait — the DOM node stays alive until your CSS animation finishes.
Driving animations with data-state
<ModalContent> and <ModalOverlay> expose data-state="open" | "closed". Target it with CSS:
[data-modal-content] {
transition: opacity 200ms, transform 200ms;
}
[data-modal-content][data-state='open'] {
opacity: 1;
transform: translateY(0);
}
[data-modal-content][data-state='closed'] {
opacity: 0;
transform: translateY(8px);
}
[data-modal-overlay] {
transition: opacity 200ms;
}
[data-modal-overlay][data-state='open'] { opacity: 1; }
[data-modal-overlay][data-state='closed'] { opacity: 0; }
- On
<ModalContent>,data-stateis"open"while this modal is the visible top of its group, and"closed"when it's leaving or covered by another modal in the same group. - On
<ModalOverlay>,data-stateis"open"while any modal in the overlay's group is active, and"closed"otherwise.
The DOM persists until the close animation finishes.
data-instant
<ModalContent> (and the topmost <ModalOverlay>) carries data-instant="" for the entire lifetime of a modal opened with instantEnter: true. Used by replaceModal for snappy wizard step swaps:
replaceModal(StepTwo, { instantEnter: true })
The bundled CSS rule is scoped to [data-state="open"], so it suppresses only the enter animation — exit animations still play normally when the modal closes.
What isn't provided
| Not provided | Reason |
|---|---|
<ModalTrigger> button | You open modals imperatively — there is no declarative trigger concept. |
| Default visual styles | These belong to your design system. |
| Animation presets | Transitions are 2–5 lines of CSS; presets impose naming and specificity. |
| Built-in close button | Presentational choice — call close() from useModalContext() on whatever element fits. |
