Concepts

Headless primitives

Why the package ships no visual styles and how to drive animations with data attributes.

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.
  • ARIArole="dialog", aria-modal, aria-labelledby/aria-describedby wired 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-state is "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-state is "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 providedReason
<ModalTrigger> buttonYou open modals imperatively — there is no declarative trigger concept.
Default visual stylesThese belong to your design system.
Animation presetsTransitions are 2–5 lines of CSS; presets impose naming and specificity.
Built-in close buttonPresentational choice — call close() from useModalContext() on whatever element fits.
Copyright © 2026