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.
- Styling based on: Modal
- Design: Figma Component
Quick Start
Import
import { AppModal } from '@pharma4u/patternlab/vue'Basic Usage
<AppModal v-model="isOpen" title="My Modal">
<p>Modal content</p>
</AppModal>Live Example
Show Code
<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.
| Class | Effect |
|---|---|
| (none) | Slides down from top (translateY(-125%) → center) |
.-anim-fade | Fades in + scales up from center (scale(0.9) → scale(1)) |
Show Code
<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
<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
<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
<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>Fullwidth Modal With Sticky Header & Footer
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
<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
<AppModal v-model="isOpen" title="Fixed Header Example" sticky-header>
<p>...</p>
</AppModal>
<AppModal v-model="isOpen" title="Normal Header">
<p>...</p>
</AppModal>Footer Variants
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
<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
| Prop | Type | Default | Description |
|---|---|---|---|
modelValue | boolean | false | Controls the visibility of the modal (v-model). |
id | string | auto-generated | Custom ID for the modal root element. |
title | string | undefined | Title text rendered in the header. Also used as aria-labelledby target if no ariaLabelledby is set. |
titleIcon | string | undefined | CSS class(es) for an icon displayed before the title. |
ariaLabel | string | undefined | Accessible label for the dialog. Use if no visible title is provided. |
ariaLabelledby | string | undefined | ID 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. |
hasBackdrop | boolean | true | Whether a backdrop overlay is shown behind the modal. |
showCloseButton | boolean | true | Whether a close button is rendered in the top-right corner. |
closeOnBackdrop | boolean | false | Whether clicking the backdrop closes the modal. |
closeOnEscape | boolean | true | Whether pressing Escape closes the modal. |
closeButtonLabel | string | 'Fenster schließen' | Accessible aria-label for the close button. |
themePlausi | boolean | true | Adds 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). |
stickyHeader | boolean | false | Pins the header to the top of the modal while its content scrolls (adds .-is-sticky to .c-panel__inner__top). |
stickyFooter | boolean | false | Pins the footer to the bottom of the modal while its content scrolls (adds .-is-sticky to .c-panel__inner__bottom). |
footerHasBg | boolean | false | Adds .-has-bg to the footer for a shaded background. |
footerSmall | boolean | false | Adds .-small to the footer for reduced padding. |
footerAlignRight | boolean | false | Adds .-align-right to right-align footer content instead of the default space-between layout. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue | boolean | Emitted when the modal requests to be closed. Used by v-model. |
open | — | Emitted immediately when the modal starts opening (before the transition). |
after-open | — | Emitted after the opening CSS transition has completed. |
close | — | Emitted when the modal closes. |
Slots
| Slot | Description |
|---|---|
default | Main content of the modal. |
header | Replaces the default title header. If provided, title and titleIcon props are ignored. |
footer | Optional footer content. Rendered inside .c-panel__inner__bottom. Only mounted when used. |
Common Patterns
With Backdrop and Close Button (default)
<AppModal v-model="isOpen" title="Default Modal">
<p>This modal has a backdrop and a close button.</p>
</AppModal>Without Backdrop
<AppModal v-model="isOpen" title="No Backdrop" :has-backdrop="false">
<p>This modal has no backdrop.</p>
</AppModal>Close on Backdrop Click
<AppModal v-model="isOpen" title="Click Outside to Close" close-on-backdrop>
<p>Click the backdrop to close this modal.</p>
</AppModal>With Footer
<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
<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
<AppModal v-model="isOpen" title="Fullwidth Modal" title-icon="c-icon -warning" size="fullwidth">
<p>This modal spans the full width.</p>
</AppModal>Sticky Header & Footer
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.
<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"witharia-modal="true". - Always provide an accessible label via
title,ariaLabel, orariaLabelledby. 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
Escapekey closes the modal by default (controlled bycloseOnEscape). - 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":
<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-openis added todocument.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.