Skip to content

Dialog

Experimental Alpha

This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.

A dialog is a modal window that overlays the main content and requires user interaction before returning to the application. Dialogs are useful for confirmations, alerts, forms, and presenting focused content.

Examples

Vue
Lit
React
Live Preview

Basic Dialog

Open Basic Dialog

This is the basic dialog content.

With Header and Footer

Open Dialog with Header/Footer

My Header

This dialog uses dialog header and footer components.

CancelConfirm

With Close Button

Open Dialog with Close Button

This dialog includes a close button in the top-right corner.

No Close on Escape

Open No Escape Dialog

Try pressing the Escape key - the dialog will not close.

Use the close button instead.

No Close on Backdrop

Open No Backdrop Close Dialog

Try clicking outside the dialog - it will not close.

Use the close button instead.

Event Handling

Open Event Dialog

Try closing this dialog in different ways:

  • Click the X button (triggers dialog-close)
  • Press Escape (triggers dialog-cancel)
  • Click the backdrop (triggers dialog-cancel)

Customized with CSS Shadow Parts

Open Customized Dialog

This dialog demonstrates CSS Shadow Parts customization with styled backdrop, container, header, heading, content, footer, and close button.

Close
View Vue Code
<template>
  <section>
    <div class="mbe4">
      <h2>Basic Dialog</h2>
    </div>
    <div class="stacked-mobile mbe4">
      <VueButton @click="showBasicDialog">Open Basic Dialog</VueButton>
      <VueDialog
        v-model:open="isBasicDialogOpen"
        heading="Basic Dialog"
        description="This is a basic dialog with heading and description."
      >
        <p>This is the basic dialog content.</p>
      </VueDialog>
    </div>

    <div class="mbe4">
      <h2>With Header and Footer</h2>
    </div>
    <div class="stacked-mobile mbe4">
      <VueButton @click="showHeaderFooterDialog">Open Dialog with Header/Footer</VueButton>
      <VueDialog v-model:open="isHeaderFooterDialogOpen">
        <VueDialogHeader>
          <h2 class="m0 p0 b0">My Header</h2>
        </VueDialogHeader>
        <p>This dialog uses dialog header and footer components.</p>
        <VueDialogFooter>
          <div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
            <VueButton @click="isHeaderFooterDialogOpen = false">Cancel</VueButton>
            <VueButton
              variant="primary"
              @click="isHeaderFooterDialogOpen = false"
            >Confirm</VueButton>
          </div>
        </VueDialogFooter>
      </VueDialog>
    </div>

    <div class="mbe4">
      <h2>With Close Button</h2>
    </div>
    <div class="stacked-mobile mbe4">
      <VueButton @click="showCloseButtonDialog">Open Dialog with Close Button</VueButton>
      <VueDialog
        v-model:open="isCloseButtonDialogOpen"
        heading="Dialog with Close Button"
        description="Click the X button to close this dialog."
        show-close-button
      >
        <p>This dialog includes a close button in the top-right corner.</p>
      </VueDialog>
    </div>

    <div class="mbe4">
      <h2>No Close on Escape</h2>
    </div>
    <div class="stacked-mobile mbe4">
      <VueButton @click="showNoEscapeDialog">Open No Escape Dialog</VueButton>
      <VueDialog
        v-model:open="isNoEscapeDialogOpen"
        heading="No Close on Escape"
        description="Pressing Escape will not close this dialog."
        no-close-on-escape
        show-close-button
      >
        <p>Try pressing the Escape key - the dialog will not close.</p>
        <p>Use the close button instead.</p>
      </VueDialog>
    </div>

    <div class="mbe4">
      <h2>No Close on Backdrop</h2>
    </div>
    <div class="stacked-mobile mbe4">
      <VueButton @click="showNoBackdropDialog">Open No Backdrop Close Dialog</VueButton>
      <VueDialog
        v-model:open="isNoBackdropDialogOpen"
        heading="No Close on Backdrop"
        description="Clicking the backdrop will not close this dialog."
        no-close-on-backdrop
        show-close-button
      >
        <p>Try clicking outside the dialog - it will not close.</p>
        <p>Use the close button instead.</p>
      </VueDialog>
    </div>

    <div class="mbe4">
      <h2>Event Handling</h2>
    </div>
    <div class="stacked-mobile mbe4">
      <VueButton @click="showEventDialog">Open Event Dialog</VueButton>
      <VueDialog
        v-model:open="isEventDialogOpen"
        heading="Event Testing"
        description="This dialog demonstrates event handling."
        show-close-button
        @dialog-open="handleDialogOpen"
        @dialog-close="handleDialogClose"
        @dialog-cancel="handleDialogCancel"
      >
        <p>Try closing this dialog in different ways:</p>
        <ul>
          <li>Click the X button (triggers dialog-close)</li>
          <li>Press Escape (triggers dialog-cancel)</li>
          <li>Click the backdrop (triggers dialog-cancel)</li>
        </ul>
        <p
          v-if="lastEvent"
          style="margin-top: 1rem; padding: 0.5rem; background: var(--ag-background-secondary); border-radius: 4px;"
        >
          Last event: <strong>{{ lastEvent }}</strong>
        </p>
      </VueDialog>
    </div>

    <div class="mbe4">
      <h2>Customized with CSS Shadow Parts</h2>
    </div>
    <div class="stacked-mobile mbe4">
      <VueButton @click="showCustomDialog">Open Customized Dialog</VueButton>
      <VueDialog
        v-model:open="isCustomDialogOpen"
        class="custom-parts-dialog"
        heading="Styled Dialog"
        description="This dialog is customized using CSS Shadow Parts."
        show-close-button
      >
        <p>This dialog demonstrates CSS Shadow Parts customization with styled backdrop, container, header, heading, content, footer, and close button.</p>
        <VueDialogFooter>
          <div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
            <VueButton @click="isCustomDialogOpen = false">Close</VueButton>
          </div>
        </VueDialogFooter>
      </VueDialog>
    </div>
  </section>
</template>

<script>
import VueDialog, {
  VueDialogHeader,
  VueDialogFooter,
} from "agnosticui-core/dialog/vue";
import VueButton from "agnosticui-core/button/vue";

export default {
  name: "DialogExamples",
  components: {
    VueDialog,
    VueDialogHeader,
    VueDialogFooter,
    VueButton,
  },
  data() {
    return {
      isBasicDialogOpen: false,
      isHeaderFooterDialogOpen: false,
      isCloseButtonDialogOpen: false,
      isNoEscapeDialogOpen: false,
      isNoBackdropDialogOpen: false,
      isEventDialogOpen: false,
      isCustomDialogOpen: false,
      lastEvent: null,
    };
  },
  methods: {
    showBasicDialog() {
      this.isBasicDialogOpen = true;
    },
    showHeaderFooterDialog() {
      this.isHeaderFooterDialogOpen = true;
    },
    showCloseButtonDialog() {
      this.isCloseButtonDialogOpen = true;
    },
    showNoEscapeDialog() {
      this.isNoEscapeDialogOpen = true;
    },
    showNoBackdropDialog() {
      this.isNoBackdropDialogOpen = true;
    },
    showEventDialog() {
      this.isEventDialogOpen = true;
      this.lastEvent = null;
    },
    showCustomDialog() {
      this.isCustomDialogOpen = true;
    },
    handleDialogOpen() {
      this.lastEvent = "dialog-open";
    },
    handleDialogClose() {
      this.lastEvent = "dialog-close";
    },
    handleDialogCancel() {
      this.lastEvent = "dialog-cancel";
    },
  },
};
</script>

<style scoped>
.custom-parts-dialog::part(ag-dialog-backdrop) {
  background: linear-gradient(
    135deg,
    rgba(102, 126, 234, 0.8) 0%,
    rgba(118, 75, 162, 0.8) 100%
  );
}

.custom-parts-dialog::part(ag-dialog-container) {
  border: 3px solid #667eea;
  box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4);
}

.custom-parts-dialog::part(ag-dialog-header) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 1.5rem;
  margin: -1.5rem -1.5rem 1rem -1.5rem;
  border-radius: 0.5rem 0.5rem 0 0;
}

.custom-parts-dialog::part(ag-dialog-heading) {
  font-size: 1.5rem;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

.custom-parts-dialog::part(ag-dialog-content) {
  padding: 0 0.5rem;
}

.custom-parts-dialog::part(ag-dialog-footer) {
  padding: 1rem;
  margin: 1rem -1.5rem -1.5rem -1.5rem;
  border-radius: 0 0 0.5rem 0.5rem;
}

.custom-parts-dialog::part(ag-dialog-close-button) {
  background: rgba(255, 255, 255, 0.2);
  color: white;
  border-radius: 50%;
  width: 2rem;
  height: 2rem;
  font-size: 1.5rem;
}

.custom-parts-dialog::part(ag-dialog-close-button):hover {
  background: rgba(255, 255, 255, 0.3);
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html, css } from 'lit';
import 'agnosticui-core/dialog';
import 'agnosticui-core/button';

export class DialogLitExamples extends LitElement {
  static styles = css`
    .custom-parts-dialog::part(ag-dialog-backdrop) {
      background: linear-gradient(
        135deg,
        rgba(102, 126, 234, 0.8) 0%,
        rgba(118, 75, 162, 0.8) 100%
      );
    }

    .custom-parts-dialog::part(ag-dialog-container) {
      border: 3px solid #667eea;
      box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4);
    }

    .custom-parts-dialog::part(ag-dialog-header) {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white;
      padding: 1.5rem;
      margin: -1.5rem -1.5rem 1rem -1.5rem;
      border-radius: 0.5rem 0.5rem 0 0;
    }

    .custom-parts-dialog::part(ag-dialog-heading) {
      font-size: 1.5rem;
      text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
    }

    .custom-parts-dialog::part(ag-dialog-content) {
      padding: 0 0.5rem;
    }

    .custom-parts-dialog::part(ag-dialog-footer) {
      padding: 1rem;
      margin: 1rem -1.5rem -1.5rem -1.5rem;
      border-radius: 0 0 0.5rem 0.5rem;
    }

    .custom-parts-dialog::part(ag-dialog-close-button) {
      background: rgba(255, 255, 255, 0.2);
      color: white;
      border-radius: 50%;
      width: 2rem;
      height: 2rem;
      font-size: 1.5rem;
    }

    .custom-parts-dialog::part(ag-dialog-close-button):hover {
      background: rgba(255, 255, 255, 0.3);
    }
  `;

  // Render in light DOM to access global utility classes
  createRenderRoot() {
    return this;
  }

  constructor() {
    super();
    this.isBasicDialogOpen = false;
    this.isHeaderFooterDialogOpen = false;
    this.isCloseButtonDialogOpen = false;
    this.isNoEscapeDialogOpen = false;
    this.isNoBackdropDialogOpen = false;
    this.isEventDialogOpen = false;
    this.isCustomDialogOpen = false;
    this.lastEvent = null;
  }

  firstUpdated() {
    // Set up event listeners for dialogs
    const eventDialog = this.querySelector('#event-dialog');
    if (eventDialog) {
      eventDialog.addEventListener('dialog-open', () => {
        this.lastEvent = 'dialog-open';
        this.requestUpdate();
      });
      eventDialog.addEventListener('dialog-close', () => {
        this.lastEvent = 'dialog-close';
        this.requestUpdate();
      });
      eventDialog.addEventListener('dialog-cancel', () => {
        this.lastEvent = 'dialog-cancel';
        this.requestUpdate();
      });
    }

    // Add close event listeners to sync state
    this.querySelectorAll('ag-dialog').forEach((dialog) => {
      dialog.addEventListener('dialog-close', (e) => {
        const dialogId = e.target.id;
        this.handleDialogClose(dialogId);
      });
      dialog.addEventListener('dialog-cancel', (e) => {
        const dialogId = e.target.id;
        this.handleDialogClose(dialogId);
      });
    });
  }

  handleDialogClose(dialogId) {
    switch (dialogId) {
      case 'basic-dialog':
        this.isBasicDialogOpen = false;
        break;
      case 'header-footer-dialog':
        this.isHeaderFooterDialogOpen = false;
        break;
      case 'close-button-dialog':
        this.isCloseButtonDialogOpen = false;
        break;
      case 'no-escape-dialog':
        this.isNoEscapeDialogOpen = false;
        break;
      case 'no-backdrop-dialog':
        this.isNoBackdropDialogOpen = false;
        break;
      case 'event-dialog':
        this.isEventDialogOpen = false;
        break;
      case 'custom-dialog':
        this.isCustomDialogOpen = false;
        break;
    }
    this.requestUpdate();
  }

  showBasicDialog() {
    this.isBasicDialogOpen = true;
    this.requestUpdate();
    setTimeout(() => {
      const dialog = this.querySelector('#basic-dialog');
      if (dialog) dialog.open = true;
    }, 0);
  }

  showHeaderFooterDialog() {
    this.isHeaderFooterDialogOpen = true;
    this.requestUpdate();
    setTimeout(() => {
      const dialog = this.querySelector('#header-footer-dialog');
      if (dialog) dialog.open = true;
    }, 0);
  }

  showCloseButtonDialog() {
    this.isCloseButtonDialogOpen = true;
    this.requestUpdate();
    setTimeout(() => {
      const dialog = this.querySelector('#close-button-dialog');
      if (dialog) dialog.open = true;
    }, 0);
  }

  showNoEscapeDialog() {
    this.isNoEscapeDialogOpen = true;
    this.requestUpdate();
    setTimeout(() => {
      const dialog = this.querySelector('#no-escape-dialog');
      if (dialog) dialog.open = true;
    }, 0);
  }

  showNoBackdropDialog() {
    this.isNoBackdropDialogOpen = true;
    this.requestUpdate();
    setTimeout(() => {
      const dialog = this.querySelector('#no-backdrop-dialog');
      if (dialog) dialog.open = true;
    }, 0);
  }

  showEventDialog() {
    this.isEventDialogOpen = true;
    this.lastEvent = null;
    this.requestUpdate();
    setTimeout(() => {
      const dialog = this.querySelector('#event-dialog');
      if (dialog) dialog.open = true;
    }, 0);
  }

  showCustomDialog() {
    this.isCustomDialogOpen = true;
    this.requestUpdate();
    setTimeout(() => {
      const dialog = this.querySelector('#custom-dialog');
      if (dialog) dialog.open = true;
    }, 0);
  }

  render() {
    return html`
      <style>
        .custom-parts-dialog::part(ag-dialog-backdrop) {
          background: linear-gradient(
            135deg,
            rgba(102, 126, 234, 0.8) 0%,
            rgba(118, 75, 162, 0.8) 100%
          );
        }

        .custom-parts-dialog::part(ag-dialog-container) {
          border: 3px solid #667eea;
          box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4);
        }

        .custom-parts-dialog::part(ag-dialog-header) {
          background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
          color: white;
          padding: 1.5rem;
          margin: -1.5rem -1.5rem 1rem -1.5rem;
          border-radius: 0.5rem 0.5rem 0 0;
        }

        .custom-parts-dialog::part(ag-dialog-heading) {
          font-size: 1.5rem;
          text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
        }

        .custom-parts-dialog::part(ag-dialog-content) {
          padding: 0 0.5rem;
        }

        .custom-parts-dialog::part(ag-dialog-footer) {
          padding: 1rem;
          margin: 1rem -1.5rem -1.5rem -1.5rem;
          border-radius: 0 0 0.5rem 0.5rem;
        }

        .custom-parts-dialog::part(ag-dialog-close-button) {
          background: rgba(255, 255, 255, 0.2);
          color: white;
          border-radius: 50%;
          width: 2rem;
          height: 2rem;
          font-size: 1.5rem;
        }

        .custom-parts-dialog::part(ag-dialog-close-button):hover {
          background: rgba(255, 255, 255, 0.3);
        }
      </style>

      <section>
        <!-- Basic Dialog -->
        <div class="mbe4">
          <h2>Basic Dialog</h2>
        </div>
        <div class="stacked-mobile mbe4">
          <ag-button @click=${this.showBasicDialog}>Open Basic Dialog</ag-button>
          <ag-dialog
            id="basic-dialog"
            heading="Basic Dialog"
            description="This is a basic dialog with heading and description."
          >
            <p>This is the basic dialog content.</p>
          </ag-dialog>
        </div>

        <!-- With Header and Footer -->
        <div class="mbe4">
          <h2>With Header and Footer</h2>
        </div>
        <div class="stacked-mobile mbe4">
          <ag-button @click=${this.showHeaderFooterDialog}>Open Dialog with Header/Footer</ag-button>
          <ag-dialog id="header-footer-dialog">
            <ag-dialog-header>
              <h2 class="m0 p0 b0">My Header</h2>
            </ag-dialog-header>
            <p>This dialog uses dialog header and footer components.</p>
            <ag-dialog-footer>
              <div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
                <ag-button @click=${() => {
                  const dialog = this.querySelector('#header-footer-dialog');
                  if (dialog) dialog.open = false;
                }}>Cancel</ag-button>
                <ag-button
                  variant="primary"
                  @click=${() => {
                    const dialog = this.querySelector('#header-footer-dialog');
                    if (dialog) dialog.open = false;
                  }}
                >Confirm</ag-button>
              </div>
            </ag-dialog-footer>
          </ag-dialog>
        </div>

        <!-- With Close Button -->
        <div class="mbe4">
          <h2>With Close Button</h2>
        </div>
        <div class="stacked-mobile mbe4">
          <ag-button @click=${this.showCloseButtonDialog}>Open Dialog with Close Button</ag-button>
          <ag-dialog
            id="close-button-dialog"
            heading="Dialog with Close Button"
            description="Click the X button to close this dialog."
            show-close-button
          >
            <p>This dialog includes a close button in the top-right corner.</p>
          </ag-dialog>
        </div>

        <!-- No Close on Escape -->
        <div class="mbe4">
          <h2>No Close on Escape</h2>
        </div>
        <div class="stacked-mobile mbe4">
          <ag-button @click=${this.showNoEscapeDialog}>Open No Escape Dialog</ag-button>
          <ag-dialog
            id="no-escape-dialog"
            heading="No Close on Escape"
            description="Pressing Escape will not close this dialog."
            no-close-on-escape
            show-close-button
          >
            <p>Try pressing the Escape key - the dialog will not close.</p>
            <p>Use the close button instead.</p>
          </ag-dialog>
        </div>

        <!-- No Close on Backdrop -->
        <div class="mbe4">
          <h2>No Close on Backdrop</h2>
        </div>
        <div class="stacked-mobile mbe4">
          <ag-button @click=${this.showNoBackdropDialog}>Open No Backdrop Close Dialog</ag-button>
          <ag-dialog
            id="no-backdrop-dialog"
            heading="No Close on Backdrop"
            description="Clicking the backdrop will not close this dialog."
            no-close-on-backdrop
            show-close-button
          >
            <p>Try clicking outside the dialog - it will not close.</p>
            <p>Use the close button instead.</p>
          </ag-dialog>
        </div>

        <!-- Event Handling -->
        <div class="mbe4">
          <h2>Event Handling</h2>
        </div>
        <div class="stacked-mobile mbe4">
          <ag-button @click=${this.showEventDialog}>Open Event Dialog</ag-button>
          <ag-dialog
            id="event-dialog"
            heading="Event Testing"
            description="This dialog demonstrates event handling."
            show-close-button
          >
            <p>Try closing this dialog in different ways:</p>
            <ul>
              <li>Click the X button (triggers dialog-close)</li>
              <li>Press Escape (triggers dialog-cancel)</li>
              <li>Click the backdrop (triggers dialog-cancel)</li>
            </ul>
            ${this.lastEvent ? html`
              <p style="margin-top: 1rem; padding: 0.5rem; background: var(--ag-background-secondary); border-radius: 4px;">
                Last event: <strong>${this.lastEvent}</strong>
              </p>
            ` : ''}
          </ag-dialog>
        </div>

        <!-- Customized with CSS Shadow Parts -->
        <div class="mbe4">
          <h2>Customized with CSS Shadow Parts</h2>
        </div>
        <div class="stacked-mobile mbe4">
          <ag-button @click=${this.showCustomDialog}>Open Customized Dialog</ag-button>
          <ag-dialog
            id="custom-dialog"
            class="custom-parts-dialog"
            heading="Styled Dialog"
            description="This dialog is customized using CSS Shadow Parts."
            show-close-button
          >
            <p>This dialog demonstrates CSS Shadow Parts customization with styled backdrop, container, header, heading, content, footer, and close button.</p>
            <ag-dialog-footer>
              <div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
                <ag-button @click=${() => {
                  const dialog = this.querySelector('#custom-dialog');
                  if (dialog) dialog.open = false;
                }}>Close</ag-button>
              </div>
            </ag-dialog-footer>
          </ag-dialog>
        </div>
      </section>
    `;
  }
}

// Register the custom element
customElements.define('dialog-lit-examples', DialogLitExamples);

Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.

View React Code
import { useState } from "react";
import ReactDialog, {
  ReactDialogHeader,
  ReactDialogFooter,
} from "agnosticui-core/dialog/react";
import { ReactButton } from "agnosticui-core/button/react";

export default function DialogReactExamples() {
  const [isBasicDialogOpen, setIsBasicDialogOpen] = useState(false);
  const [isHeaderFooterDialogOpen, setIsHeaderFooterDialogOpen] = useState(false);
  const [isCloseButtonDialogOpen, setIsCloseButtonDialogOpen] = useState(false);
  const [isNoEscapeDialogOpen, setIsNoEscapeDialogOpen] = useState(false);
  const [isNoBackdropDialogOpen, setIsNoBackdropDialogOpen] = useState(false);
  const [isEventDialogOpen, setIsEventDialogOpen] = useState(false);
  const [isCustomDialogOpen, setIsCustomDialogOpen] = useState(false);
  const [lastEvent, setLastEvent] = useState(null);

  const handleDialogOpen = () => {
    setLastEvent("dialog-open");
  };

  const handleDialogClose = () => {
    setLastEvent("dialog-close");
  };

  const handleDialogCancel = () => {
    setLastEvent("dialog-cancel");
  };

  const showEventDialog = () => {
    setIsEventDialogOpen(true);
    setLastEvent(null);
  };

  return (
    <section>
      {/* Basic Dialog */}
      <div className="mbe4">
        <h2>Basic Dialog</h2>
      </div>
      <div className="stacked-mobile mbe4">
        <ReactButton onClick={() => setIsBasicDialogOpen(true)}>
          Open Basic Dialog
        </ReactButton>
        <ReactDialog
          open={isBasicDialogOpen}
          onOpenChange={setIsBasicDialogOpen}
          heading="Basic Dialog"
          description="This is a basic dialog with heading and description."
        >
          <p>This is the basic dialog content.</p>
        </ReactDialog>
      </div>

      {/* With Header and Footer */}
      <div className="mbe4">
        <h2>With Header and Footer</h2>
      </div>
      <div className="stacked-mobile mbe4">
        <ReactButton onClick={() => setIsHeaderFooterDialogOpen(true)}>
          Open Dialog with Header/Footer
        </ReactButton>
        <ReactDialog
          open={isHeaderFooterDialogOpen}
          onOpenChange={setIsHeaderFooterDialogOpen}
        >
          <ReactDialogHeader>
            <h2 className="m0 p0 b0">My Header</h2>
          </ReactDialogHeader>
          <p>This dialog uses dialog header and footer components.</p>
          <ReactDialogFooter>
            <div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end" }}>
              <ReactButton onClick={() => setIsHeaderFooterDialogOpen(false)}>
                Cancel
              </ReactButton>
              <ReactButton
                variant="primary"
                onClick={() => setIsHeaderFooterDialogOpen(false)}
              >
                Confirm
              </ReactButton>
            </div>
          </ReactDialogFooter>
        </ReactDialog>
      </div>

      {/* With Close Button */}
      <div className="mbe4">
        <h2>With Close Button</h2>
      </div>
      <div className="stacked-mobile mbe4">
        <ReactButton onClick={() => setIsCloseButtonDialogOpen(true)}>
          Open Dialog with Close Button
        </ReactButton>
        <ReactDialog
          open={isCloseButtonDialogOpen}
          onOpenChange={setIsCloseButtonDialogOpen}
          heading="Dialog with Close Button"
          description="Click the X button to close this dialog."
          showCloseButton
        >
          <p>This dialog includes a close button in the top-right corner.</p>
        </ReactDialog>
      </div>

      {/* No Close on Escape */}
      <div className="mbe4">
        <h2>No Close on Escape</h2>
      </div>
      <div className="stacked-mobile mbe4">
        <ReactButton onClick={() => setIsNoEscapeDialogOpen(true)}>
          Open No Escape Dialog
        </ReactButton>
        <ReactDialog
          open={isNoEscapeDialogOpen}
          onOpenChange={setIsNoEscapeDialogOpen}
          heading="No Close on Escape"
          description="Pressing Escape will not close this dialog."
          noCloseOnEscape
          showCloseButton
        >
          <p>Try pressing the Escape key - the dialog will not close.</p>
          <p>Use the close button instead.</p>
        </ReactDialog>
      </div>

      {/* No Close on Backdrop */}
      <div className="mbe4">
        <h2>No Close on Backdrop</h2>
      </div>
      <div className="stacked-mobile mbe4">
        <ReactButton onClick={() => setIsNoBackdropDialogOpen(true)}>
          Open No Backdrop Close Dialog
        </ReactButton>
        <ReactDialog
          open={isNoBackdropDialogOpen}
          onOpenChange={setIsNoBackdropDialogOpen}
          heading="No Close on Backdrop"
          description="Clicking the backdrop will not close this dialog."
          noCloseOnBackdrop
          showCloseButton
        >
          <p>Try clicking outside the dialog - it will not close.</p>
          <p>Use the close button instead.</p>
        </ReactDialog>
      </div>

      {/* Event Handling */}
      <div className="mbe4">
        <h2>Event Handling</h2>
      </div>
      <div className="stacked-mobile mbe4">
        <ReactButton onClick={showEventDialog}>Open Event Dialog</ReactButton>
        <ReactDialog
          open={isEventDialogOpen}
          onOpenChange={setIsEventDialogOpen}
          heading="Event Testing"
          description="This dialog demonstrates event handling."
          showCloseButton
          onDialogOpen={handleDialogOpen}
          onDialogClose={handleDialogClose}
          onDialogCancel={handleDialogCancel}
        >
          <p>Try closing this dialog in different ways:</p>
          <ul>
            <li>Click the X button (triggers dialog-close)</li>
            <li>Press Escape (triggers dialog-cancel)</li>
            <li>Click the backdrop (triggers dialog-cancel)</li>
          </ul>
          {lastEvent && (
            <p
              style={{
                marginTop: "1rem",
                padding: "0.5rem",
                background: "var(--ag-background-secondary)",
                borderRadius: "4px",
              }}
            >
              Last event: <strong>{lastEvent}</strong>
            </p>
          )}
        </ReactDialog>
      </div>

      {/* Customized with CSS Shadow Parts */}
      <div className="mbe4">
        <h2>Customized with CSS Shadow Parts</h2>
      </div>
      <div className="stacked-mobile mbe4">
        <ReactButton onClick={() => setIsCustomDialogOpen(true)}>
          Open Customized Dialog
        </ReactButton>
        <ReactDialog
          open={isCustomDialogOpen}
          onOpenChange={setIsCustomDialogOpen}
          className="custom-parts-dialog"
          heading="Styled Dialog"
          description="This dialog is customized using CSS Shadow Parts."
          showCloseButton
        >
          <p>
            This dialog demonstrates CSS Shadow Parts customization with styled
            backdrop, container, header, heading, content, footer, and close button.
          </p>
          <ReactDialogFooter>
            <div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end" }}>
              <ReactButton onClick={() => setIsCustomDialogOpen(false)}>
                Close
              </ReactButton>
            </div>
          </ReactDialogFooter>
        </ReactDialog>
      </div>
    </section>
  );
}
Open in StackBlitz

Usage

TIP

The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:

bash
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add Dialog

The CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.

Vue
vue
<template>
  <section>
    <VueButton @click="showDialog">Open Dialog</VueButton>
    <VueDialog
      v-model:open="isOpen"
      heading="Dialog Title"
      description="This is a dialog description"
      @dialog-close="handleClose"
    >
      <p>This is the dialog content.</p>
    </VueDialog>

    <VueButton @click="showCustomDialog">Open Custom Dialog</VueButton>
    <VueDialog v-model:open="isCustomOpen">
      <VueDialogHeader>
        <h2 style="margin: 0;">Custom Header</h2>
      </VueDialogHeader>
      <p>Dialog with custom header and footer.</p>
      <VueDialogFooter>
        <div style="display: flex; gap: 0.5rem; justify-content: flex-end;">
          <VueButton @click="isCustomOpen = false">Cancel</VueButton>
          <VueButton variant="primary" @click="isCustomOpen = false"
            >Confirm</VueButton
          >
        </div>
      </VueDialogFooter>
    </VueDialog>

    <VueButton @click="showCloseButtonDialog">Open Dialog</VueButton>
    <VueDialog
      v-model:open="isCloseButtonOpen"
      heading="Dialog with Close Button"
      show-close-button
    >
      <p>This dialog includes a close button.</p>
    </VueDialog>

    <VueButton @click="showEventDialog">Open Dialog</VueButton>
    <VueDialog
      v-model:open="isEventOpen"
      heading="Event Testing"
      show-close-button
      @dialog-open="handleOpen"
      @dialog-close="handleClose"
      @dialog-cancel="handleCancel"
    >
      <p>Try closing this dialog different ways to see events.</p>
    </VueDialog>
  </section>
</template>

<script>
import VueDialog, {
  VueDialogHeader,
  VueDialogFooter,
} from "agnosticui-core/dialog/vue";
import VueButton from "agnosticui-core/button/vue";

export default {
  components: {
    VueDialog,
    VueDialogHeader,
    VueDialogFooter,
    VueButton,
  },
  data() {
    return {
      isOpen: false,
      isCustomOpen: false,
      isCloseButtonOpen: false,
      isEventOpen: false,
    };
  },
  methods: {
    showDialog() {
      this.isOpen = true;
    },
    showCustomDialog() {
      this.isCustomOpen = true;
    },
    showCloseButtonDialog() {
      this.isCloseButtonOpen = true;
    },
    showEventDialog() {
      this.isEventOpen = true;
    },
    handleOpen() {
      console.log("Dialog opened");
    },
    handleClose() {
      console.log("Dialog closed");
    },
    handleCancel() {
      console.log("Dialog cancelled");
    },
  },
};
</script>
React
tsx
import { useState } from "react";
import {
  ReactDialog,
  DialogHeader,
  DialogFooter,
} from "agnosticui-core/dialog/react";

export default function DialogExample() {
  const [isOpen, setIsOpen] = useState(false);
  const [isCustomOpen, setIsCustomOpen] = useState(false);
  const [isCloseButtonOpen, setIsCloseButtonOpen] = useState(false);

  const handleClose = () => {
    console.log("Dialog closed");
    setIsOpen(false);
  };

  const handleCancel = () => {
    console.log("Dialog cancelled");
    setIsOpen(false);
  };

  return (
    <section>
      <button onClick={() => setIsOpen(true)}>Open Dialog</button>
      <ReactDialog
        open={isOpen}
        heading="Dialog Title"
        description="This is a dialog description"
        onDialogClose={handleClose}
        onDialogCancel={handleCancel}
      >
        <p>This is the dialog content.</p>
      </ReactDialog>

      <button onClick={() => setIsCustomOpen(true)}>Open Custom Dialog</button>
      <ReactDialog
        open={isCustomOpen}
        onDialogClose={() => setIsCustomOpen(false)}
        onDialogCancel={() => setIsCustomOpen(false)}
      >
        <DialogHeader>
          <h2 style={{ margin: 0 }}>Custom Header</h2>
        </DialogHeader>
        <p>Dialog with custom header and footer.</p>
        <DialogFooter>
          <div
            style={{
              display: "flex",
              gap: "0.5rem",
              justifyContent: "flex-end",
            }}
          >
            <button onClick={() => setIsCustomOpen(false)}>Cancel</button>
            <button onClick={() => setIsCustomOpen(false)}>Confirm</button>
          </div>
        </DialogFooter>
      </ReactDialog>

      <button onClick={() => setIsCloseButtonOpen(true)}>Open Dialog</button>
      <ReactDialog
        open={isCloseButtonOpen}
        heading="Dialog with Close Button"
        showCloseButton={true}
        onDialogClose={() => setIsCloseButtonOpen(false)}
        onDialogCancel={() => setIsCloseButtonOpen(false)}
      >
        <p>This dialog includes a close button.</p>
      </ReactDialog>
    </section>
  );
}
Lit (Web Components)
html
<script type="module">
  import 'agnosticui-core/dialog';

  @customElement('my-element')
  export class MyElement extends LitElement {
    firstUpdated() {
      const dialog = this.shadowRoot?.querySelector('#my-dialog') as any;
      const openButton = this.shadowRoot?.querySelector('#open-dialog');

      openButton?.addEventListener('click', () => {
        if (dialog) {
          dialog.open = true;
        }
      });

      dialog?.addEventListener('dialog-close', () => {
        dialog.open = false;
        console.log('Dialog closed');
      });

      dialog?.addEventListener('dialog-cancel', () => {
        dialog.open = false;
        console.log('Dialog cancelled');
      });
    }
    // ... rest of your class ...
  }
</script>

<section>
  <button id="open-dialog">Open Dialog</button>
  <ag-dialog
    id="my-dialog"
    heading="Dialog Title"
    description="This is a dialog description"
  >
    <p>This is the dialog content.</p>
  </ag-dialog>

  <ag-dialog id="custom-dialog">
    <div slot="header">
      <h2 style="margin: 0;">Custom Header</h2>
    </div>
    <p>Dialog with custom header and footer.</p>
    <div
      slot="footer"
      style="display: flex; gap: 0.5rem; justify-content: flex-end;"
    >
      <button>Cancel</button>
      <button>Confirm</button>
    </div>
  </ag-dialog>

  <ag-dialog heading="Dialog with Close Button" show-close-button>
    <p>This dialog includes a close button.</p>
  </ag-dialog>
</section>

Props

PropTypeDefaultDescription
openbooleanfalseWhether the dialog is open
headingstring''The heading text for the dialog
descriptionstring''The description text for the dialog
noCloseOnEscapebooleanfalsePrevents closing the dialog when pressing the Escape key
noCloseOnBackdropbooleanfalsePrevents closing the dialog when clicking the backdrop
showCloseButtonbooleanfalseShows a close button (×) in the top-right corner of the dialog

Events

EventFrameworkDetailDescription
dialog-openVue: @dialog-open
React: onDialogOpen
Lit: @dialog-open
voidFired when the dialog opens.
dialog-closeVue: @dialog-close
React: onDialogClose
Lit: @dialog-close
voidFired when the dialog closes via the close button.
dialog-cancelVue: @dialog-cancel
React: onDialogCancel
Lit: @dialog-cancel
voidFired when the dialog is cancelled (Escape key or backdrop click).

Event Handling Examples

Vue:

vue
<VueDialog
  v-model:open="isOpen"
  @dialog-open="handleOpen"
  @dialog-close="handleClose"
  @dialog-cancel="handleCancel"
>
  <p>Dialog content</p>
</VueDialog>

React:

tsx
<ReactDialog
  open={isOpen}
  onDialogOpen={(e) => console.log("Dialog opened", e)}
  onDialogClose={(e) => setIsOpen(false)}
  onDialogCancel={(e) => setIsOpen(false)}
>
  <p>Dialog content</p>
</ReactDialog>

Lit:

html
<script>
  const dialog = document.querySelector("ag-dialog");
  dialog.addEventListener("dialog-open", (e) => {
    console.log("Dialog opened", e);
  });
  dialog.addEventListener("dialog-close", (e) => {
    console.log("Dialog closed", e);
  });
</script>

<ag-dialog id="my-dialog"></ag-dialog>
<script>
  const dialog = document.querySelector("#my-dialog");
  dialog.onDialogOpen = (e) => console.log("Dialog opened", e);
  dialog.onDialogClose = (e) => console.log("Dialog closed", e);
</script>

Slots

Vue

  • Default slot: Main content of the dialog
  • VueDialogHeader: Custom header content (replaces heading prop when used)
  • VueDialogFooter: Footer content for action buttons

React

  • children: Main content of the dialog
  • DialogHeader: Custom header content (replaces heading prop when used)
  • DialogFooter: Footer content for action buttons

Lit

  • Default slot: Main content of the dialog
  • slot="header": Custom header content (replaces heading prop when used)
  • slot="footer": Footer content for action buttons

CSS Shadow Parts

Shadow Parts allow you to style internal elements of the dialog from outside the shadow DOM using the ::part() CSS selector.

PartDescription
ag-dialog-backdropThe backdrop overlay element behind the dialog
ag-dialog-containerThe main dialog container that holds all dialog content
ag-dialog-headerThe header section wrapper
ag-dialog-headingThe heading text element (when using heading prop)
ag-dialog-close-buttonThe close button (when showCloseButton is true)
ag-dialog-contentThe main content section wrapper
ag-dialog-footerThe footer section wrapper

Customization Example

css
ag-dialog::part(ag-dialog-backdrop) {
  background: linear-gradient(
    135deg,
    rgba(102, 126, 234, 0.8) 0%,
    rgba(118, 75, 162, 0.8) 100%
  );
}

ag-dialog::part(ag-dialog-container) {
  background: linear-gradient(to bottom, #ffffff, #f0f4ff);
  border: 3px solid #667eea;
  box-shadow: 0 20px 60px rgba(102, 126, 234, 0.4);
}

ag-dialog::part(ag-dialog-header) {
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  padding: 1.5rem;
}

ag-dialog::part(ag-dialog-heading) {
  font-size: 1.5rem;
  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}

ag-dialog::part(ag-dialog-close-button) {
  background: rgba(255, 255, 255, 0.2);
  color: white;
  border-radius: 50%;
}

ag-dialog::part(ag-dialog-content) {
  padding: 0 0.5rem;
}

ag-dialog::part(ag-dialog-footer) {
  background: #f0f4ff;
  padding: 1rem;
}

Accessibility

The Dialog implements the WAI-ARIA Dialog (Modal) Pattern:

  • Uses role="dialog" and aria-modal="true" for proper screen reader announcement
  • Implements focus trap to keep keyboard focus within the dialog
  • Pressing Escape closes the dialog (unless noCloseOnEscape is true)
  • Clicking the backdrop closes the dialog (unless noCloseOnBackdrop is true)
  • Returns focus to the triggering element when closed
  • Prevents background scroll when dialog is open
  • Close button has aria-label="Close dialog" for screen readers
  • Dialog can be labeled via heading prop or custom header with proper heading element
  • Keyboard navigation cycles through all focusable elements within the dialog
  • Supports Tab and Shift+Tab for navigation within the focus trap

Best Practices

  • Always provide a heading (via heading prop or custom header) for accessibility
  • Use showCloseButton or provide explicit close actions in footer
  • For critical actions, consider setting noCloseOnBackdrop and noCloseOnEscape to prevent accidental dismissal
  • Ensure focus is managed properly by using semantic buttons for triggers
  • Keep dialog content concise and focused on a single task

v-model Support (Vue)

The Vue Dialog component supports v-model:open for two-way binding:

vue
<VueDialog v-model:open="isDialogOpen">
  <p>Dialog content</p>
</VueDialog>

This automatically syncs the dialog's open state with your component's data.