Modal
Displays dialog content on top of the main page, requiring user interaction.
First release: 1.0.0 Latest update: July 1, 2025 Current version: 1.0.0
Import
import { PlusModal } from '@plusui/react';import { PlusModalComponent } from '@plusui/angular';import { PlusModal } from '@plusui/vue';import PlusModal from '@plusui/svelte';<script src="https://cdn.jsdelivr.net/npm/@plusui/core"></script>Package
@plusui/react@plusui/angular@plusui/vue@plusui/svelte@plusui/coreDocs
Changelog
Modal Changelog
Recent changes and updates for the modal component
v1.0.0
Added Modal component for dialogs
- Multiple sizes: sm, md, lg
- Custom header, body, and footer slots
- Backdrop click to close
- Keyboard navigation (Escape to close)
- Accessibility with ARIA attributes
The Modal component displays a dialog window on top of the main content. It’s used to focus the user’s attention on a specific task, information, or action without navigating away from the current page. Modals typically include a header, body, footer, and a close mechanism.
Anatomy
Section titled “Anatomy”
Basic Example
Section titled “Basic Example”To use the modal, you typically need a trigger element (like a button) to open it and controls within the modal to close it. The modal’s visibility is controlled programmatically using its show() and hide() methods.
import { PlusModal, PlusButton } from '@plusui/react';import { useRef } from 'react';
export default () => { const modalRef = useRef<HTMLPlusModalElement>(null);
const openModal = () => { modalRef.current?.show(); };
const closeModal = () => { modalRef.current?.hide(); };
const handleHide = () => { console.log('Modal hidden'); };
return ( <> <PlusButton onClick={openModal}>Open Modal</PlusButton> <PlusModal ref={modalRef} onPlusModalHide={handleHide}> <div slot="header">Modal Title</div> <div slot="body">This is the main content of the modal.</div> <div slot="footer"> <PlusButton kind="text" onClick={closeModal}> Close </PlusButton> <PlusButton status="primary">Save Changes</PlusButton> </div> </PlusModal> </> );};import { Component, ViewChild, ElementRef } from '@angular/core';
@Component({ selector: 'app-modal-basic', template: \` <plus-button (plus-click)="openModal()">Open Modal</plus-button> <plus-modal #modal (plus-modal-hide)="handleHide()"> <div slot="header">Modal Title</div> <div slot="body">This is the main content of the modal.</div> <div slot="footer"> <plus-button kind="text" (plus-click)="closeModal()">Close</plus-button> <plus-button status="primary">Save Changes</plus-button> </div> </plus-modal> \`})export class ModalBasicComponent { @ViewChild('modal') modalRef!: ElementRef<HTMLPlusModalElement>;
openModal() { this.modalRef.nativeElement.show(); }
closeModal() { this.modalRef.nativeElement.hide(); }
handleHide() { console.log('Modal hidden'); }}<template> <plus-button @plus-click="openModal">Open Modal</plus-button> <plus-modal ref="modalRef" @plus-modal-hide="handleHide"> <div slot="header">Modal Title</div> <div slot="body">This is the main content of the modal.</div> <div slot="footer"> <plus-button kind="text" @plus-click="closeModal">Close</plus-button> <plus-button status="primary">Save Changes</plus-button> </div> </plus-modal></template>
<script setup>import { ref } from 'vue';
const modalRef = ref(null);
const openModal = () => { modalRef.value?.show();};
const closeModal = () => { modalRef.value?.hide();};
const handleHide = () => { console.log('Modal hidden');};</script><script> import { PlusModal, PlusButton } from '@plusui/svelte';
let modalRef;
const openModal = () => { modalRef?.show(); };
const closeModal = () => { modalRef?.hide(); };
const handleHide = () => { console.log('Modal hidden'); };</script>
<PlusButton on:plus-click={openModal}>Open Modal</PlusButton><PlusModal bind:this={modalRef} on:plus-modal-hide={handleHide}> <div slot="header">Modal Title</div> <div slot="body">This is the main content of the modal.</div> <div slot="footer"> <PlusButton kind="text" on:plus-click={closeModal}>Close</PlusButton> <PlusButton status="primary">Save Changes</PlusButton> </div></PlusModal><plus-button id="open-modal-basic">Open Modal</plus-button>
<plus-modal id="my-modal"> <div slot="header">Modal Title</div> <div slot="body">This is the main content of the modal.</div> <div slot="footer"> <plus-button id="close-modal-basic" kind="text">Close</plus-button> <plus-button status="primary">Save Changes</plus-button> </div></plus-modal>
<script> const modal = document.getElementById('my-modal'); const openButton = document.getElementById('open-modal-basic'); const closeButton = document.getElementById('close-modal-basic');
openButton.addEventListener('plus-click', () => modal.show()); closeButton.addEventListener('plus-click', () => modal.hide());
modal.addEventListener('plus-modal-hide', () => { console.log('Modal hidden'); });</script>The size prop controls the width of the modal dialog.
import { PlusModal, PlusButton } from '@plusui/react';import { useRef } from 'react';
export default () => { const smallModalRef = useRef<HTMLPlusModalElement>(null); const largeModalRef = useRef<HTMLPlusModalElement>(null); const fullModalRef = useRef<HTMLPlusModalElement>(null);
return ( <div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}> <PlusButton onClick={() => smallModalRef.current?.show()}>Small</PlusButton> <PlusModal ref={smallModalRef} size="sm"> <div slot="header">Small Modal</div> <div slot="body">Content for small modal...</div> <div slot="footer"> <PlusButton onClick={() => smallModalRef.current?.hide()}>Close</PlusButton> </div> </PlusModal>
<PlusButton onClick={() => largeModalRef.current?.show()}>Large</PlusButton> <PlusModal ref={largeModalRef} size="lg"> <div slot="header">Large Modal</div> <div slot="body">Content for large modal...</div> <div slot="footer"> <PlusButton onClick={() => largeModalRef.current?.hide()}>Close</PlusButton> </div> </PlusModal>
<PlusButton onClick={() => fullModalRef.current?.show()}>Full Width</PlusButton> <PlusModal ref={fullModalRef} size="full"> <div slot="header">Full Width Modal</div> <div slot="body">Content for full width modal...</div> <div slot="footer"> <PlusButton onClick={() => fullModalRef.current?.hide()}>Close</PlusButton> </div> </PlusModal> </div> );};import { Component, ViewChild, ElementRef } from '@angular/core';
@Component({ selector: 'app-modal-sizes', template: \` <div style="display: flex; gap: 10px; flex-wrap: wrap;"> <plus-button (plus-click)="openSmallModal()">Small</plus-button> <plus-button (plus-click)="openLargeModal()">Large</plus-button> <plus-button (plus-click)="openFullModal()">Full Width</plus-button> </div>
<plus-modal #smallModal size="sm"> <div slot="header">Small Modal</div> <div slot="body">Content for small modal...</div> <div slot="footer"> <plus-button (plus-click)="closeSmallModal()">Close</plus-button> </div> </plus-modal>
<plus-modal #largeModal size="lg"> <div slot="header">Large Modal</div> <div slot="body">Content for large modal...</div> <div slot="footer"> <plus-button (plus-click)="closeLargeModal()">Close</plus-button> </div> </plus-modal>
<plus-modal #fullModal size="full"> <div slot="header">Full Width Modal</div> <div slot="body">Content for full width modal...</div> <div slot="footer"> <plus-button (plus-click)="closeFullModal()">Close</plus-button> </div> </plus-modal> \`})export class ModalSizesComponent { @ViewChild('smallModal') smallModalRef!: ElementRef<HTMLPlusModalElement>; @ViewChild('largeModal') largeModalRef!: ElementRef<HTMLPlusModalElement>; @ViewChild('fullModal') fullModalRef!: ElementRef<HTMLPlusModalElement>;
openSmallModal() { this.smallModalRef.nativeElement.show(); } closeSmallModal() { this.smallModalRef.nativeElement.hide(); } openLargeModal() { this.largeModalRef.nativeElement.show(); } closeLargeModal() { this.largeModalRef.nativeElement.hide(); } openFullModal() { this.fullModalRef.nativeElement.show(); } closeFullModal() { this.fullModalRef.nativeElement.hide(); }}<template> <div style="display: flex; gap: 10px; flex-wrap: wrap;"> <plus-button @plus-click="openSmallModal">Small</plus-button> <plus-button @plus-click="openLargeModal">Large</plus-button> <plus-button @plus-click="openFullModal">Full Width</plus-button> </div>
<plus-modal ref="smallModalRef" size="sm"> <div slot="header">Small Modal</div> <div slot="body">Content for small modal...</div> <div slot="footer"> <plus-button @plus-click="closeSmallModal">Close</plus-button> </div> </plus-modal>
<plus-modal ref="largeModalRef" size="lg"> <div slot="header">Large Modal</div> <div slot="body">Content for large modal...</div> <div slot="footer"> <plus-button @plus-click="closeLargeModal">Close</plus-button> </div> </plus-modal>
<plus-modal ref="fullModalRef" size="full"> <div slot="header">Full Width Modal</div> <div slot="body">Content for full width modal...</div> <div slot="footer"> <plus-button @plus-click="closeFullModal">Close</plus-button> </div> </plus-modal></template>
<script setup>import { ref } from 'vue';
const smallModalRef = ref(null);const largeModalRef = ref(null);const fullModalRef = ref(null);
const openSmallModal = () => smallModalRef.value?.show();const closeSmallModal = () => smallModalRef.value?.hide();const openLargeModal = () => largeModalRef.value?.show();const closeLargeModal = () => largeModalRef.value?.hide();const openFullModal = () => fullModalRef.value?.show();const closeFullModal = () => fullModalRef.value?.hide();</script><script> import { PlusModal, PlusButton } from '@plusui/svelte';
let smallModalRef, largeModalRef, fullModalRef;
const openSmallModal = () => smallModalRef?.show(); const closeSmallModal = () => smallModalRef?.hide(); const openLargeModal = () => largeModalRef?.show(); const closeLargeModal = () => largeModalRef?.hide(); const openFullModal = () => fullModalRef?.show(); const closeFullModal = () => fullModalRef?.hide();</script>
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> <PlusButton on:plus-click={openSmallModal}>Small</PlusButton> <PlusButton on:plus-click={openLargeModal}>Large</PlusButton> <PlusButton on:plus-click={openFullModal}>Full Width</PlusButton></div>
<PlusModal bind:this={smallModalRef} size="sm"> <div slot="header">Small Modal</div> <div slot="body">Content for small modal...</div> <div slot="footer"> <PlusButton on:plus-click={closeSmallModal}>Close</PlusButton> </div></PlusModal>
<PlusModal bind:this={largeModalRef} size="lg"> <div slot="header">Large Modal</div> <div slot="body">Content for large modal...</div> <div slot="footer"> <PlusButton on:plus-click={closeLargeModal}>Close</PlusButton> </div></PlusModal>
<PlusModal bind:this={fullModalRef} size="full"> <div slot="header">Full Width Modal</div> <div slot="body">Content for full width modal...</div> <div slot="footer"> <PlusButton on:plus-click={closeFullModal}>Close</PlusButton> </div></PlusModal><plus-button id="open-sm-2">Small</plus-button><plus-modal id="modal-sm-2" size="sm"> <div slot="header">Small Modal</div> <div slot="body">Content for small modal...</div> <div slot="footer"><plus-button id="close-sm-2">Close</plus-button></div></plus-modal>
<plus-button id="open-lg-2">Large</plus-button><plus-modal id="modal-lg-2" size="lg"> <div slot="header">Large Modal</div> <div slot="body">Content for large modal...</div> <div slot="footer"><plus-button id="close-lg-2">Close</plus-button></div></plus-modal>
<plus-button id="open-full-2">Full Width</plus-button><plus-modal id="modal-full-2" size="full"> <div slot="header">Full Width Modal</div> <div slot="body">Content for full width modal...</div> <div slot="footer"><plus-button id="close-full-2">Close</plus-button></div></plus-modal>
<script> document.getElementById('open-sm-2').addEventListener('plus-click', () => document.getElementById('modal-sm-2').show()); document.getElementById('close-sm-2').addEventListener('plus-click', () => document.getElementById('modal-sm-2').hide()); document.getElementById('open-lg-2').addEventListener('plus-click', () => document.getElementById('modal-lg-2').show()); document.getElementById('close-lg-2').addEventListener('plus-click', () => document.getElementById('modal-lg-2').hide()); document.getElementById('open-full-2').addEventListener('plus-click', () => document.getElementById('modal-full-2').show()); document.getElementById('close-full-2').addEventListener('plus-click', () => document.getElementById('modal-full-2').hide());</script>Closing Behavior
Section titled “Closing Behavior”Control how the modal closes using close-on-backdrop and close-on-esc.
import { PlusModal, PlusButton } from '@plusui/react';import { useRef } from 'react';
export default () => { const noBackdropModalRef = useRef<HTMLPlusModalElement>(null); const noEscModalRef = useRef<HTMLPlusModalElement>(null);
return ( <div style={{ display: 'flex', gap: '10px' }}> <PlusButton onClick={() => noBackdropModalRef.current?.show()}> No Backdrop Close </PlusButton> <PlusModal ref={noBackdropModalRef} closeOnBackdrop={false}> <div slot="header">No Backdrop Close</div> <div slot="body">You must use the close button or Esc key.</div> <div slot="footer"> <PlusButton onClick={() => noBackdropModalRef.current?.hide()}> Close </PlusButton> </div> </PlusModal>
<PlusButton onClick={() => noEscModalRef.current?.show()}> No Esc Close </PlusButton> <PlusModal ref={noEscModalRef} closeOnEsc={false}> <div slot="header">No Esc Close</div> <div slot="body">You must use the close button or click the backdrop.</div> <div slot="footer"> <PlusButton onClick={() => noEscModalRef.current?.hide()}> Close </PlusButton> </div> </PlusModal> </div> );};import { Component, ViewChild, ElementRef } from '@angular/core';
@Component({ selector: 'app-modal-closing', template: \` <div style="display: flex; gap: 10px;"> <plus-button (plus-click)="openNoBackdropModal()">No Backdrop Close</plus-button> <plus-button (plus-click)="openNoEscModal()">No Esc Close</plus-button> </div>
<plus-modal #noBackdropModal [close-on-backdrop]="false"> <div slot="header">No Backdrop Close</div> <div slot="body">You must use the close button or Esc key.</div> <div slot="footer"> <plus-button (plus-click)="closeNoBackdropModal()">Close</plus-button> </div> </plus-modal>
<plus-modal #noEscModal [close-on-esc]="false"> <div slot="header">No Esc Close</div> <div slot="body">You must use the close button or click the backdrop.</div> <div slot="footer"> <plus-button (plus-click)="closeNoEscModal()">Close</plus-button> </div> </plus-modal> \`})export class ModalClosingComponent { @ViewChild('noBackdropModal') noBackdropModalRef!: ElementRef<HTMLPlusModalElement>; @ViewChild('noEscModal') noEscModalRef!: ElementRef<HTMLPlusModalElement>;
openNoBackdropModal() { this.noBackdropModalRef.nativeElement.show(); } closeNoBackdropModal() { this.noBackdropModalRef.nativeElement.hide(); } openNoEscModal() { this.noEscModalRef.nativeElement.show(); } closeNoEscModal() { this.noEscModalRef.nativeElement.hide(); }}<template> <div style="display: flex; gap: 10px;"> <plus-button @plus-click="openNoBackdropModal">No Backdrop Close</plus-button> <plus-button @plus-click="openNoEscModal">No Esc Close</plus-button> </div>
<plus-modal ref="noBackdropModalRef" :close-on-backdrop="false"> <div slot="header">No Backdrop Close</div> <div slot="body">You must use the close button or Esc key.</div> <div slot="footer"> <plus-button @plus-click="closeNoBackdropModal">Close</plus-button> </div> </plus-modal>
<plus-modal ref="noEscModalRef" :close-on-esc="false"> <div slot="header">No Esc Close</div> <div slot="body">You must use the close button or click the backdrop.</div> <div slot="footer"> <plus-button @plus-click="closeNoEscModal">Close</plus-button> </div> </plus-modal></template>
<script setup>import { ref } from 'vue';
const noBackdropModalRef = ref(null);const noEscModalRef = ref(null);
const openNoBackdropModal = () => noBackdropModalRef.value?.show();const closeNoBackdropModal = () => noBackdropModalRef.value?.hide();const openNoEscModal = () => noEscModalRef.value?.show();const closeNoEscModal = () => noEscModalRef.value?.hide();</script><script> import { PlusModal, PlusButton } from '@plusui/svelte';
let noBackdropModalRef, noEscModalRef;
const openNoBackdropModal = () => noBackdropModalRef?.show(); const closeNoBackdropModal = () => noBackdropModalRef?.hide(); const openNoEscModal = () => noEscModalRef?.show(); const closeNoEscModal = () => noEscModalRef?.hide();</script>
<div style="display: flex; gap: 10px;"> <PlusButton on:plus-click={openNoBackdropModal}>No Backdrop Close</PlusButton> <PlusButton on:plus-click={openNoEscModal}>No Esc Close</PlusButton></div>
<PlusModal bind:this={noBackdropModalRef} closeOnBackdrop={false}> <div slot="header">No Backdrop Close</div> <div slot="body">You must use the close button or Esc key.</div> <div slot="footer"> <PlusButton on:plus-click={closeNoBackdropModal}>Close</PlusButton> </div></PlusModal>
<PlusModal bind:this={noEscModalRef} closeOnEsc={false}> <div slot="header">No Esc Close</div> <div slot="body">You must use the close button or click the backdrop.</div> <div slot="footer"> <PlusButton on:plus-click={closeNoEscModal}>Close</PlusButton> </div></PlusModal><!-- This modal won't close by clicking the background --><plus-button id="open-no-backdrop-3">No Backdrop Close</plus-button><plus-modal id="modal-no-backdrop-3" close-on-backdrop="false"> <div slot="header">No Backdrop Close</div> <div slot="body">You must use the close button or Esc key.</div> <div slot="footer"><plus-button id="close-no-backdrop-3">Close</plus-button></div></plus-modal>
<!-- This modal won't close by pressing Escape --><plus-button id="open-no-esc-3">No Esc Close</plus-button><plus-modal id="modal-no-esc-3" close-on-esc="false"> <div slot="header">No Esc Close</div> <div slot="body">You must use the close button or click the backdrop.</div> <div slot="footer"><plus-button id="close-no-esc-3">Close</plus-button></div></plus-modal>
<script> document.getElementById('open-no-backdrop-3').addEventListener('plus-click', () => document.getElementById('modal-no-backdrop-3').show()); document.getElementById('close-no-backdrop-3').addEventListener('plus-click', () => document.getElementById('modal-no-backdrop-3').hide()); document.getElementById('open-no-esc-3').addEventListener('plus-click', () => document.getElementById('modal-no-esc-3').show()); document.getElementById('close-no-esc-3').addEventListener('plus-click', () => document.getElementById('modal-no-esc-3').hide());</script>Visual Sections
Section titled “Visual Sections”Layout & Spacing
Section titled “Layout & Spacing”
Light & Dark Mode
Section titled “Light & Dark Mode”
Accessibility (a11y)
Section titled “Accessibility (a11y)”- Keyboard Behavior:
- When the modal is open, focus should be trapped within the modal. Users should be able to navigate interactive elements inside the modal using
TabandShift+Tab. (Note: Focus trapping needs to be implemented by the developer or a utility library, as it’s not built into the component’s provided code snippet). - The modal can be closed using the
Escapekey (ifclose-on-escis true). - The close button (
<slot name="close">or default) should be focusable and activatable withEnterorSpace.
- When the modal is open, focus should be trapped within the modal. Users should be able to navigate interactive elements inside the modal using
- Screen Reader:
- The component uses
role="dialog"andaria-modal="true"to indicate its purpose and that interaction with the underlying page is blocked. aria-hiddenis toggled based on theisOpenstate to hide/show the modal from assistive technologies.- It’s recommended to set an accessible name for the modal using
aria-labelledbypointing to the header slot’s content ID, oraria-labeldirectly on theplus-modalelement, especially if the header is complex or absent. - The default close button has an
aria-label="Close modal". If providing a custom close button via thecloseslot, ensure it also has an appropriate accessible label.
- The component uses
- Required Developer Actions:
- Implement focus trapping when the modal is open to prevent users from tabbing outside the modal content.
- Provide a clear and descriptive header (
<slot name="header">) or usearia-label/aria-labelledbyon theplus-modalelement. - Ensure interactive elements within the modal body and footer are keyboard accessible.
- If using a custom close button in the
closeslot, provide an appropriatearia-label.
API Reference
Section titled “API Reference”Properties
Section titled “Properties”| Name | Type | Default | Description | Required |
|---|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'full' | 'md' | The size (width) of the modal. | No |
isOpen | boolean | false | Controls whether the modal is currently visible. | No |
fullWidth | boolean | false | If true, the modal ignores the size prop and takes full width. | No |
closeOnBackdrop | boolean | true | Allows closing the modal by clicking the background overlay. | No |
closeOnEsc | boolean | true | Allows closing the modal by pressing the Escape key. | No |
animationDuration | number | 300 | Duration of the open/close animation in milliseconds. | No |
Events
Section titled “Events”| Name | Payload Type | Description |
|---|---|---|
plus-modal-before-show | CustomEvent<void> | Fired just before the modal starts to open. |
plus-modal-show | CustomEvent<void> | Fired after the modal has fully opened. |
plus-modal-before-hide | CustomEvent<void> | Fired just before the modal starts to close. |
plus-modal-hide | CustomEvent<void> | Fired after the modal has fully closed. |
Methods
Section titled “Methods”| Name | Parameters | Returns | Description |
|---|---|---|---|
show() | — | void | Opens the modal. Triggers plus-modal-before-show and plus-modal-show. |
hide() | — | void | Closes the modal. Triggers plus-modal-before-hide and plus-modal-hide. |
| Name | Description |
|---|---|
header | Content for the modal’s header area. |
body | The main content area of the modal. |
default (unnamed) | Also used for the main content if body slot is not used. |
footer | Content for the modal’s footer area (typically for buttons). |
close | Custom content for the close button (replaces the default X). |
CSS Shadow Parts
Section titled “CSS Shadow Parts”| Part | Description |
|---|---|
container | The main container element (includes overlay and modal). |
overlay | The background overlay element. |
modal | The modal dialog window itself. |
header | The header section of the modal. |
body | The main content body section. |
footer | The footer section of the modal. |
close-button | The close button element (default or slotted). |
Join the Community
Plus UI is built by the community. Join us on our platforms to contribute, get help, and stay up to date.