Skip to content

AppModal (Vue Component)

A Modal is a dialog box or popup window that is displayed on top of the current page.

The AppModal component is rendered via Teleport and supports accessibility, focus trapping, CSS transitions, and multiple sizes.

Quick Start

Import

ts
import { AppModal } from '@pharma4u/patternlab/vue'

Basic Usage

ts
<AppModal v-model="isOpen" title="My Modal">
  <p>Modal content</p>
</AppModal>

Live Example

Show Code
vue
<script setup>
  import { AppModal, BasicButton } from '@pharma4u/patternlab';
  import { ref } from 'vue';
  const isOpen = ref(false);
</script>
<template>
  <BasicButton @click="isOpen = true" size="small">Open Modal</BasicButton>
  <AppModal v-model="isOpen" title="My Modal">
    <p>Modal content</p>
  </AppModal>
</template>

Animation Variants

Two animation styles are available. Add .-anim-fade to the .c-panel element to use the new one — existing modals are unaffected.
This animation is set as default for large, extra large and fullwidth modals.

ClassEffect
(none)Slides down from top (translateY(-125%) → center)
.-anim-fadeFades in + scales up from center (scale(0.9)scale(1))
Show Code
vue
<script setup>
  import { AppModal, BasicButton } from '@pharma4u/patternlab/vue';
  import { ref } from 'vue';

  const isOpenSlide = ref(false);
  const isOpenFade = ref(false);
</script>

<template>
  <BasicButton @click="isOpenSlide = true" size="small">Slide from Top (default)</BasicButton>
  <BasicButton @click="isOpenFade = true" size="small">Fade + Scale (new)</BasicButton>

  <AppModal v-model="isOpenSlide" title="Slide from Top">
    <p>Default animation.</p>
  </AppModal>

  <AppModal v-model="isOpenFade" title="Fade + Scale" animation="fade">
    <p>New animation.</p>
  </AppModal>
</template>

Size Variants

The following examples show the different size modifiers for the modal. The default size is 'medium'.

Small Modal

A small modal without a visible backdrop or close button, dismissible by clicking outside it, the confirm button, or the escape key:
size="small" :has-backdrop="false" close-on-backdrop :show-close-button="false"

Show Code
vue
<AppModal
  v-model="isOpen"
  title="Small Modal"
  title-icon="far fa-exclamation-triangle"
  size="small"
  :has-backdrop="false"
  close-on-backdrop
  :show-close-button="false"
  footer-small
  footer-align-right
>
  <p>This modal is dismissible by clicking outside it or pressing the button or escape key.</p>
  <template #footer>
    <button data-dismiss="modal">Verstanden</button>
  </template>
</AppModal>

Large Modal

A large modal with a visible backdrop (not dismissible by clicking on it) and a footer with a shaded background:
size="large" has-backdrop footer-has-bg

Show Code
vue
<AppModal
  v-model="isOpen"
  title="Large Modal with Backdrop"
  title-icon="far fa-cat-space m-r-s"
  size="large"
  has-backdrop
  footer-has-bg
>
  <h4>This large modal has a custom close button and is not dismissible by clicking outside of it.</h4>
  <p>...</p>
  <table class="c-table">...</table>
  <template #footer>
    <button data-dismiss="modal">Abbrechen</button>
    <button data-dismiss="modal">Speichern</button>
  </template>
</AppModal>

Extra Large Modal

An extra large modal with sticky header, normal footer and scrollable content:
size="extra-large" sticky-header footer-has-bg

Show Code
vue
<AppModal
  v-model="isOpen"
  title="XL Modal with Sticky Header & Normal Footer"
  title-icon="far fa-2x fa-cat-space"
  size="extra-large"
  close-on-backdrop
  has-backdrop
  sticky-header
  footer-has-bg
  @open="initAccordion"
>
  <h4>This extra large modal has a custom close button and is dismissible by clicking outside of it.</h4>
  <p>...</p>
  <div class="js-accordion-group c-accordion-wrapper" role="tablist" aria-multiselectable="true">
    <div class="c-accordion -large">...</div>
    <div class="c-accordion -large">...</div>
  </div>
  <template #footer>
    <button data-dismiss="modal">Abbrechen</button>
    <button data-dismiss="modal">Speichern</button>
  </template>
</AppModal>

A "fullwidth" modal (max-width on really large viewports) with both a sticky header and a sticky footer:
size="fullwidth" sticky-header sticky-footer footer-small

Show Code
vue
<AppModal
  v-model="isOpen"
  title="Fullwidth Modal with Sticky Header & Footer"
  title-icon="far fa-2x fa-cat-space"
  size="fullwidth"
  has-backdrop
  close-on-backdrop
  sticky-header
  sticky-footer
  footer-small
  @open="initAccordion"
>
  <h3>...</h3>
  <p>... long scrollable content ...</p>
  <div class="js-accordion-group c-accordion-wrapper" role="tablist" aria-multiselectable="true">
    <div class="c-accordion">...</div>
    <div class="c-accordion">...</div>
  </div>
  <template #footer>
    <button data-dismiss="modal">Abbrechen</button>
    <button data-dismiss="modal">Speichern</button>
  </template>
</AppModal>

TIP

The accordion markup above is plain vanilla .c-accordion HTML (there is no Vue accordion component yet). Because it's only teleported into the DOM once the modal opens, Accordion.init() is called again on the @open event.

Header Variants

sticky-header pins the header (icon, title, close button) to the top while the content scrolls underneath it.

Show Code
vue
<AppModal v-model="isOpen" title="Fixed Header Example" sticky-header>
  <p>...</p>
</AppModal>

<AppModal v-model="isOpen" title="Normal Header">
  <p>...</p>
</AppModal>

footer-* props control the footer's appearance: sticky-footer pins it to the bottom, footer-has-bg adds a shaded background, footer-small reduces its padding, and footer-align-right right-aligns its content instead of the default space-between layout.

Show Code
vue
<AppModal v-model="isOpen" title="Sticky Footer" sticky-footer>
  <p>...</p>
  <template #footer>...</template>
</AppModal>

<AppModal v-model="isOpen" title="Has Background" footer-has-bg>
  <p>...</p>
  <template #footer>...</template>
</AppModal>

<AppModal v-model="isOpen" title="Has Background + Small" footer-has-bg footer-small>
  <p>...</p>
  <template #footer>...</template>
</AppModal>

<AppModal v-model="isOpen" title="Align Right" footer-align-right>
  <p>...</p>
  <template #footer>...</template>
</AppModal>

Props

PropTypeDefaultDescription
modelValuebooleanfalseControls the visibility of the modal (v-model).
idstringauto-generatedCustom ID for the modal root element.
titlestringundefinedTitle text rendered in the header. Also used as aria-labelledby target if no ariaLabelledby is set.
titleIconstringundefinedCSS class(es) for an icon displayed before the title.
ariaLabelstringundefinedAccessible label for the dialog. Use if no visible title is provided.
ariaLabelledbystringundefinedID of an external element that labels the dialog. Overrides the auto-generated title reference.
size'small' | 'medium' | 'large' | 'extra-large' | 'fullwidth''medium'Controls the width of the modal.
hasBackdropbooleantrueWhether a backdrop overlay is shown behind the modal.
showCloseButtonbooleantrueWhether a close button is rendered in the top-right corner.
closeOnBackdropbooleanfalseWhether clicking the backdrop closes the modal.
closeOnEscapebooleantrueWhether pressing Escape closes the modal.
closeButtonLabelstring'Fenster schließen'Accessible aria-label for the close button.
themePlausibooleantrueAdds the theme-plausi CSS class to the modal root element. Set to false to opt out.
animation'slide' | 'fade''slide'Controls the open animation. 'slide' drops in from the top; 'fade' fades and scales in from the center (adds .-anim-fade).
stickyHeaderbooleanfalsePins the header to the top of the modal while its content scrolls (adds .-is-sticky to .c-panel__inner__top).
stickyFooterbooleanfalsePins the footer to the bottom of the modal while its content scrolls (adds .-is-sticky to .c-panel__inner__bottom).
footerHasBgbooleanfalseAdds .-has-bg to the footer for a shaded background.
footerSmallbooleanfalseAdds .-small to the footer for reduced padding.
footerAlignRightbooleanfalseAdds .-align-right to right-align footer content instead of the default space-between layout.

Events

EventPayloadDescription
update:modelValuebooleanEmitted when the modal requests to be closed. Used by v-model.
openEmitted immediately when the modal starts opening (before the transition).
after-openEmitted after the opening CSS transition has completed.
closeEmitted when the modal closes.

Slots

SlotDescription
defaultMain content of the modal.
headerReplaces the default title header. If provided, title and titleIcon props are ignored.
footerOptional footer content. Rendered inside .c-panel__inner__bottom. Only mounted when used.

Common Patterns

With Backdrop and Close Button (default)

vue
<AppModal v-model="isOpen" title="Default Modal">
  <p>This modal has a backdrop and a close button.</p>
</AppModal>

Without Backdrop

vue
<AppModal v-model="isOpen" title="No Backdrop" :has-backdrop="false">
  <p>This modal has no backdrop.</p>
</AppModal>

Close on Backdrop Click

vue
<AppModal v-model="isOpen" title="Click Outside to Close" close-on-backdrop>
  <p>Click the backdrop to close this modal.</p>
</AppModal>
vue
<AppModal v-model="isOpen" title="Confirm Action">
  <p>Do you want to proceed?</p>
  <template #footer>
    <button data-dismiss="modal">Cancel</button>
    <button @click="onConfirm">Confirm</button>
  </template>
</AppModal>

Custom Header

vue
<AppModal v-model="isOpen" aria-labelledby="custom-header">
  <template #header>
    <h2 id="custom-header">Custom Header</h2>
  </template>
  <p>The header slot replaces the default title entirely.</p>
</AppModal>

Fullwidth with Icon

vue
<AppModal v-model="isOpen" title="Fullwidth Modal" title-icon="c-icon -warning" size="fullwidth">
  <p>This modal spans the full width.</p>
</AppModal>

Useful for long, scrollable content. When either stickyHeader or stickyFooter is set, content is automatically wrapped in .c-panel__inner__body so the padding math lines up with the fixed header/footer. See Fullwidth Modal With Sticky Header & Footer above for a live example.

vue
<AppModal
  v-model="isOpen"
  title="Fullwidth Modal with Sticky Header & Footer"
  size="fullwidth"
  sticky-header
  sticky-footer
  footer-small
>
  <p>... long scrollable content ...</p>
  <template #footer>
    <button data-dismiss="modal">Abbrechen</button>
    <button @click="onSave">Speichern</button>
  </template>
</AppModal>

Accessibility

  • The modal renders as role="dialog" with aria-modal="true".
  • Always provide an accessible label via title, ariaLabel, or ariaLabelledby. A console warning is issued if none of these are set.
  • Focus is trapped inside the modal while it is open.
  • Focus returns to the previously focused element when the modal closes.
  • The Escape key closes the modal by default (controlled by closeOnEscape).
  • Any element inside the modal with data-dismiss="modal" will also trigger close on click.

Programmatic Close

Any element inside the modal can trigger a close by setting data-dismiss="modal":

vue
<AppModal v-model="isOpen" title="Confirm">
  <p>Are you sure?</p>
  <template #footer>
    <button data-dismiss="modal">Cancel</button>
    <button @click="confirm">Confirm</button>
  </template>
</AppModal>

Notes

  • The modal is rendered via <Teleport to="body">, so it always appears outside the component tree.
  • While the modal is open, .-modal-open is added to document.body. Multiple modals are tracked — the class is only removed when all are closed.
  • The component is unmount-safe: open state and event listeners are cleaned up in onBeforeUnmount.