Skip to content

Modal Component

A dialog overlay component that slides in from the top with a smooth transition. Supports breadcrumb-style titles, customizable action buttons, auto-sizing based on content height, and dynamic z-index stacking for nested modal support.


Table of Contents


Import

ts
import { Modal } from "dolphin-components";
import type { ModalProps, ModalTitle, ModalAction } from "dolphin-components";

Basic Usage

vue
<template>
  <button @click="showModal = true" class="btn btn-primary">Open Modal</button>

  <Modal
    :show="showModal"
    :title="[{ name: 'Edit User' }]"
    :actions="[
      { title: 'Cancel', emit: 'onClose', class: 'btn btn-outline-secondary' },
      { title: 'Save', emit: 'onSave', class: 'btn btn-primary' },
    ]"
    @onClose="showModal = false"
    @onSave="handleSave"
  >
    <p>Modal content goes here.</p>
  </Modal>
</template>

<script setup lang="ts">
import { ref } from "vue";
import { Modal } from "dolphin-components";

const showModal = ref(false);

const handleSave = () => {
  console.log("Saving...");
  showModal.value = false;
};
</script>

Props

PropTypeDefaultDescription
showbooleanfalseControls visibility of the modal.
titleModalTitle[][]Breadcrumb-style title array. Each item can be plain text or a router link.
actionsModalAction[][]Footer action buttons. Each button emits the specified event on click.
widthstring"600px"CSS width of the modal panel (e.g. "600px", "80%").
showClosebooleantrueShow or hide the close (×) button in the top-right corner.

Events

EventPayloadDescription
onCloseEmitted when the close button or a "close" action is clicked.
[custom emit]string | numberCustom events from action buttons. The event name comes from the emit property of the action.

Slots

Slot NameDescription
defaultThe main content body of the modal.

TypeScript Interfaces

typescript
interface ModalTitle {
  name: string;     // Display text
  link?: string;    // Optional Vue Router link path. Use '#' for a clickable link that emits onClose.
}

interface ModalAction {
  emit: string;     // Event name to emit when clicked
  title: string;    // Button label text
  class?: string;   // CSS class for the button (default: 'btn btn-primary')
}

interface ModalProps {
  title: ModalTitle[];
  actions?: ModalAction[];
  show: boolean;
  width?: string;
  showClose?: boolean;
}

Usage Examples

With Breadcrumb Title

vue
<Modal
  :show="showModal"
  :title="[
    { name: 'Users', link: '/users' },
    { name: 'Edit User' },
  ]"
  :actions="[{ title: 'Close', emit: 'onClose' }]"
  @onClose="showModal = false"
>
  <p>Breadcrumb navigation shows: Users > Edit User</p>
</Modal>

Custom Width

vue
<Modal
  :show="showWideModal"
  :title="[{ name: 'Report Preview' }]"
  width="900px"
  @onClose="showWideModal = false"
>
  <div>Wide modal content for data tables, charts, etc.</div>
</Modal>

Without Close Button

vue
<Modal
  :show="showConfirm"
  :title="[{ name: 'Confirm Action' }]"
  :showClose="false"
  :actions="[
    { title: 'Cancel', emit: 'onClose', class: 'btn btn-outline-secondary' },
    { title: 'Confirm', emit: 'onConfirm', class: 'btn btn-danger' },
  ]"
  @onClose="showConfirm = false"
  @onConfirm="handleConfirm"
>
  <p>Are you sure you want to delete this record?</p>
</Modal>

Nested Modals

The Modal component supports opening multiple modals simultaneously. Each new modal automatically receives a higher z-index so it stacks correctly on top of the previous one.

vue
<template>
  <button @click="showFirst = true" class="btn btn-primary">Open Modal</button>

  <Modal
    :show="showFirst"
    :title="[{ name: 'First Modal' }]"
    :actions="[
      { title: 'Open Nested', emit: 'openNested', class: 'btn btn-primary' },
      { title: 'Close', emit: 'onClose' },
    ]"
    @onClose="showFirst = false"
    @openNested="showSecond = true"
  >
    <p>This is the first modal.</p>
  </Modal>

  <Modal
    :show="showSecond"
    :title="[{ name: 'First Modal', link: '#' }, { name: 'Nested Modal' }]"
    :actions="[{ title: 'Close', emit: 'onClose' }]"
    @onClose="showSecond = false"
  >
    <p>This modal appears on top of the first one.</p>
  </Modal>
</template>

<script setup>
import { ref } from "vue";
import { Modal } from "dolphin-components";

const showFirst = ref(false);
const showSecond = ref(false);
</script>

With TypeScript

vue
<script setup lang="ts">
import { ref } from "vue";
import { Modal, type ModalTitle, type ModalAction } from "dolphin-components";

const showModal = ref(false);

const title: ModalTitle[] = [
  { name: "Settings", link: "/settings" },
  { name: "Profile" },
];

const actions: ModalAction[] = [
  { title: "Cancel", emit: "onClose", class: "btn btn-outline-secondary" },
  { title: "Save Changes", emit: "onSave", class: "btn btn-primary" },
];

const handleSave = () => {
  // Save logic
  showModal.value = false;
};
</script>

<template>
  <Modal
    :show="showModal"
    :title="title"
    :actions="actions"
    @onClose="showModal = false"
    @onSave="handleSave"
  >
    <form>
      <label>Name</label>
      <input class="input-text" />
    </form>
  </Modal>
</template>

Dynamic Z-Index Stacking

Added in v3.1.18

When multiple modals are open at the same time, the component automatically calculates the z-index based on how many .app-modal-panel elements are currently in the DOM:

  • Base z-index: 11
  • Each subsequent modal: increments by 10 (e.g., 21, 31, …)
  • Backdrop z-index: always modal z-index - 1

This means you can freely nest modals without worrying about z-index conflicts. The calculation runs when show becomes true and after the enter transition completes.


Component Behavior

Auto Overflow

The modal body automatically becomes scrollable when its content exceeds window.innerHeight - 200px. A MutationObserver and ResizeObserver watch for content changes to dynamically apply or remove the scroll behavior.

Transition Animation

The modal uses a modal-load transition:

  • Enter: Slides down from above with a fade-in (300ms ease)
  • Leave: Slides up with a fade-out (300ms ease)

Teleport

The modal is rendered via <Teleport to="body"> to avoid CSS stacking context issues with parent containers.


Best Practices

  1. Always handle onClose — Bind @onClose to set show back to false. Otherwise the modal cannot be dismissed.
  2. Use breadcrumb titles for context — When opening a modal from a detail page, include the parent page name as a link for navigation context.
  3. Use link: '#' — Setting link to '#' on a title item makes it clickable and emits onClose when clicked (useful for breadcrumb "back" navigation).
  4. Keep content height manageable — The modal automatically scrolls, but very tall content may benefit from internal tabs or pagination.
  5. Match action button classes — Use btn btn-primary for primary actions and btn btn-outline-secondary for cancel/dismiss actions.

Troubleshooting

The backdrop is a visual overlay only. To close on backdrop click, add a click handler to the backdrop or use the close button / action buttons.

The modal uses z-index: 11 (base). If your app has elements with higher z-index values, the modal stacking may be affected. The dynamic z-index calculation helps with multiple modals but won't override external high-z-index elements.

Content not scrolling

The overflow detection runs via observers. If your content loads asynchronously, the modal will detect the height change and apply scroll. Ensure your content is inside the default slot.


Screenshot

modal image