component

Dialog

A flexible dialog component for displaying modal content, overlays, and drawer interfaces. Supports both modal and non-modal modes, drawer positioning, scroll locking, and stacked dialogs with cascade animations.

Default

A centered modal dialog with title, content, and a close button.

Dialog Title

Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex placeat unde natus ut ea voluptatum vel officiis commodi optio a totam repellendus, fuga, laborum voluptatibus recusandae eos. Voluptate, vel non!
---
import Dialog from "../dialog.astro";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { Icon } from "astro-icon/components";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column", align: "start" })}>
  <Dialog id="astro-demo-dialog-default" title="Dialog Title">
    <Stamp
      slot="heading-leading"
      size="sm"
      shape="circle"
      feedback="primary"
      emphasis="secondary"
    >
      <Icon name="lucide:info" />
    </Stamp>
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Ex placeat unde natus
    ut ea voluptatum vel officiis commodi optio a totam repellendus, fuga, laborum
    voluptatibus recusandae eos. Voluptate, vel non!
  </Dialog>

  <Button id="astro-demo-dialog-trigger">Open Dialog</Button>
</div>

<script>
  document
    .getElementById("astro-demo-dialog-trigger")
    ?.addEventListener("click", () => {
      window.__PindobaDialogManager?.open("astro-demo-dialog-default");
    });
</script>

The dialog header renders a Banner internally, so every Banner slot is available directly on Dialog. Use headingLeading / headingTrailing (inline with the title), subheadingLeading / subheadingTrailing (inline with the subtitle), and Banner’s outer leading / trailing slots for content that sits beside the whole heading group — the latter is the right place for header actions now that Dialog no longer defines its own headerActions slot.

Scheduled Maintenance

System will be unavailable on March 15, 2026

All services will be offline from 02:00 to 04:00 UTC. No action is required — your data will remain intact throughout the maintenance window.

File Manager

12 files

Browse and manage your project files

Select a file to preview, rename, move, or remove it from your project workspace.

Account Settings

Manage your preferences and security

Verified
Update your display name, email address, and two-factor authentication settings from this panel.

Notifications

3

3 unread messages

You have new notifications about team activity, mentions, and system updates. Review them here or mark everything as read.

Filters

Choose the filters you want to apply to the current view. Changes take effect immediately once applied.
---
import Dialog from "../dialog.astro";
import Button from "@pindoba/astro-button";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Icon } from "astro-icon/components";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "row", flexWrap: "wrap" })}>
  <!-- Subtitle only -->
  <Dialog
    id="astro-demo-dialog-header-notice"
    title="Scheduled Maintenance"
    subtitle="System will be unavailable on March 15, 2026"
  >
    All services will be offline from 02:00 to 04:00 UTC. No action is required
    — your data will remain intact throughout the maintenance window.
  </Dialog>

  <Button id="astro-demo-dialog-header-notice-trigger">With Subtitle</Button>

  <!-- Stamp leading + Badge trailing -->
  <Dialog
    id="astro-demo-dialog-header-files"
    title="File Manager"
    subtitle="Browse and manage your project files"
  >
    <Stamp
      slot="heading-leading"
      shape="square"
      background="surface.deep"
      size="sm"
    >
      <Icon name="lucide:folder" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm">12 files</Badge>
    Select a file to preview, rename, move, or remove it from your project workspace.
  </Dialog>

  <Button id="astro-demo-dialog-header-files-trigger">Stamp + Badge</Button>

  <!-- Stamp in heading + subheading leading, Badge in subheading trailing -->
  <Dialog
    id="astro-demo-dialog-header-account"
    title="Account Settings"
    subtitle="Manage your preferences and security"
  >
    <Stamp
      slot="heading-leading"
      feedback="primary"
      emphasis="secondary"
      shape="circle"
      size="sm"
    >
      <Icon name="lucide:user" />
    </Stamp>
    <Badge slot="subheading-trailing" size="sm" feedback="success"
      >Verified</Badge
    >
    Update your display name, email address, and two-factor authentication settings
    from this panel.
  </Dialog>

  <Button id="astro-demo-dialog-header-account-trigger">Subheading Slots</Button
  >

  <!-- Banner trailing slot as a header action (no dedicated dialog slot) -->
  <Dialog
    id="astro-demo-dialog-header-notifications"
    title="Notifications"
    subtitle="3 unread messages"
  >
    <Stamp
      slot="heading-leading"
      feedback="warning"
      emphasis="secondary"
      shape="circle"
      size="sm"
    >
      <Icon name="lucide:bell" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm" feedback="warning">3</Badge>
    <Button slot="trailing" size="sm" emphasis="ghost">Mark all read</Button>
    You have new notifications about team activity, mentions, and system updates.
    Review them here or mark everything as read.
  </Dialog>

  <Button id="astro-demo-dialog-header-notifications-trigger"
    >Trailing Action</Button
  >

  <!-- Stamp + actions, no subtitle -->
  <Dialog id="astro-demo-dialog-header-filters" title="Filters">
    <Stamp
      slot="heading-leading"
      feedback="primary"
      emphasis="secondary"
      shape="square"
      size="sm"
    >
      <Icon name="lucide:filter" />
    </Stamp>
    <div slot="trailing" class={stack({ direction: "row", gap: "2xs" })}>
      <Button size="sm" emphasis="ghost">Reset</Button>
      <Button size="sm" emphasis="secondary">Apply</Button>
    </div>
    Choose the filters you want to apply to the current view. Changes take effect
    immediately once applied.
  </Dialog>

  <Button id="astro-demo-dialog-header-filters-trigger"
    >Actions, No Subtitle</Button
  >
</div>

<script>
  const ids = [
    "astro-demo-dialog-header-notice",
    "astro-demo-dialog-header-files",
    "astro-demo-dialog-header-account",
    "astro-demo-dialog-header-notifications",
    "astro-demo-dialog-header-filters",
  ];
  ids.forEach((id) => {
    document.getElementById(`${id}-trigger`)?.addEventListener("click", () => {
      window.__PindobaDialogManager?.open(id);
    });
  });
</script>

Drawer

The drawer prop positions the dialog along an edge of the screen. Drawers animate in from their respective side and default to removing the border radius on the attached edge.

Right Drawer

right
This drawer slides in from the right edge of the screen.

Left Drawer

left
This drawer slides in from the left edge of the screen.

Top Drawer

top
This drawer slides in from the top edge of the screen.

Bottom Drawer

bottom
This drawer slides in from the bottom edge of the screen.
---
import Dialog from "../dialog.astro";
import Button from "@pindoba/astro-button";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Icon } from "astro-icon/components";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "sm", direction: "row", flexWrap: "wrap" })}>
  <Dialog id="astro-demo-drawer-right" title="Right Drawer" drawer="right">
    <Stamp
      slot="heading-leading"
      shape="square"
      size="sm"
      feedback="primary"
      emphasis="secondary"
    >
      <Icon name="lucide:panel-right" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm">right</Badge>
    This drawer slides in from the right edge of the screen.
  </Dialog>

  <Dialog id="astro-demo-drawer-left" title="Left Drawer" drawer="left">
    <Stamp
      slot="heading-leading"
      shape="square"
      size="sm"
      feedback="primary"
      emphasis="secondary"
    >
      <Icon name="lucide:panel-left" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm">left</Badge>
    This drawer slides in from the left edge of the screen.
  </Dialog>

  <Dialog id="astro-demo-drawer-top" title="Top Drawer" drawer="top">
    <Stamp
      slot="heading-leading"
      shape="square"
      size="sm"
      feedback="primary"
      emphasis="secondary"
    >
      <Icon name="lucide:panel-top" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm">top</Badge>
    This drawer slides in from the top edge of the screen.
  </Dialog>

  <Dialog id="astro-demo-drawer-bottom" title="Bottom Drawer" drawer="bottom">
    <Stamp
      slot="heading-leading"
      shape="square"
      size="sm"
      feedback="primary"
      emphasis="secondary"
    >
      <Icon name="lucide:panel-bottom" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm">bottom</Badge>
    This drawer slides in from the bottom edge of the screen.
  </Dialog>

  <Button id="astro-demo-drawer-right-trigger" emphasis="secondary"
    >Right</Button
  >
  <Button id="astro-demo-drawer-left-trigger" emphasis="secondary">Left</Button>
  <Button id="astro-demo-drawer-top-trigger" emphasis="secondary">Top</Button>
  <Button id="astro-demo-drawer-bottom-trigger" emphasis="secondary"
    >Bottom</Button
  >
</div>

<script>
  const triggers: [string, string][] = [
    ["astro-demo-drawer-right-trigger", "astro-demo-drawer-right"],
    ["astro-demo-drawer-left-trigger", "astro-demo-drawer-left"],
    ["astro-demo-drawer-top-trigger", "astro-demo-drawer-top"],
    ["astro-demo-drawer-bottom-trigger", "astro-demo-drawer-bottom"],
  ];

  triggers.forEach(([triggerId, dialogId]) => {
    document.getElementById(triggerId)?.addEventListener("click", () => {
      window.__PindobaDialogManager?.open(dialogId);
    });
  });
</script>

Nested Drawers

Multiple dialogs can be stacked. Lower dialogs scale back and dim when a new one opens on top, creating a cascade effect. Each dialog is independently closable.

Open multiple drawers to see the cascade effect. Each drawer slides inward and dims when another opens on top.

Settings

1

This is the first level drawer. Click below to open another drawer on top.

Item 1: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Item 2: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Item 3: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Item 4: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Item 5: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Item 6: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Item 7: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Item 8: Lorem ipsum dolor sit amet, consectetur adipiscing elit.

Edit Profile

2

This is the second level. Notice how the first drawer moved back and dimmed.

Profile field 1: Some value here.

Profile field 2: Some value here.

Profile field 3: Some value here.

Profile field 4: Some value here.

Profile field 5: Some value here.

Profile field 6: Some value here.

Confirmation

3

This is the third level. All previous drawers are stacked behind.

---
import Dialog from "../dialog.astro";
import Button from "@pindoba/astro-button";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Icon } from "astro-icon/components";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---

<div class={stack({ gap: "md", direction: "column", align: "start" })}>
  <p class={css({ color: "neutral.text.subtle", fontSize: "sm" })}>
    Open multiple drawers to see the cascade effect. Each drawer slides inward
    and dims when another opens on top.
  </p>

  <Dialog id="astro-demo-nested-drawer-1" title="Settings" drawer="right">
    <Stamp slot="heading-leading" shape="circle" size="sm" emphasis="secondary">
      <Icon name="lucide:settings" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm">1</Badge>
    <div class={stack({ gap: "md" })}>
      <p>
        This is the first level drawer. Click below to open another drawer on
        top.
      </p>
      <Button id="astro-demo-nested-drawer-2-trigger" emphasis="secondary">
        Open Second Drawer
      </Button>
      {
        Array.from({ length: 8 }, (_, i) => (
          <p class={css({ color: "neutral.text.subtle" })}>
            Item {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing
            elit.
          </p>
        ))
      }
    </div>
  </Dialog>

  <Dialog id="astro-demo-nested-drawer-2" title="Edit Profile" drawer="right">
    <Stamp
      slot="heading-leading"
      shape="circle"
      size="sm"
      feedback="primary"
      emphasis="secondary"
    >
      <Icon name="lucide:user" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm" feedback="primary">2</Badge>
    <div class={stack({ gap: "md" })}>
      <p>
        This is the second level. Notice how the first drawer moved back and
        dimmed.
      </p>
      <div class={stack({ gap: "sm" })}>
        <Button id="astro-demo-nested-drawer-3-trigger" emphasis="secondary">
          Open Third Drawer
        </Button>
        <Button id="astro-demo-nested-drawer-2-close" emphasis="ghost">
          Close This Drawer
        </Button>
      </div>
      {
        Array.from({ length: 6 }, (_, i) => (
          <p class={css({ color: "neutral.text.subtle" })}>
            Profile field {i + 1}: Some value here.
          </p>
        ))
      }
    </div>
  </Dialog>

  <Dialog id="astro-demo-nested-drawer-3" title="Confirmation" drawer="right">
    <Stamp
      slot="heading-leading"
      shape="circle"
      size="sm"
      feedback="success"
      emphasis="secondary"
    >
      <Icon name="lucide:check-circle-2" />
    </Stamp>
    <Badge slot="heading-trailing" size="sm" feedback="success">3</Badge>
    <div class={stack({ gap: "md" })}>
      <p>This is the third level. All previous drawers are stacked behind.</p>
      <div class={stack({ gap: "sm" })}>
        <Button id="astro-demo-nested-drawer-3-confirm" emphasis="primary"
          >Confirm</Button
        >
        <Button id="astro-demo-nested-drawer-3-cancel" emphasis="ghost"
          >Cancel</Button
        >
      </div>
    </div>
  </Dialog>

  <Button id="astro-demo-nested-drawer-1-trigger">Open First Drawer</Button>
</div>

<script>
  const triggers: [string, string, "open" | "close"][] = [
    [
      "astro-demo-nested-drawer-1-trigger",
      "astro-demo-nested-drawer-1",
      "open",
    ],
    [
      "astro-demo-nested-drawer-2-trigger",
      "astro-demo-nested-drawer-2",
      "open",
    ],
    [
      "astro-demo-nested-drawer-3-trigger",
      "astro-demo-nested-drawer-3",
      "open",
    ],
    ["astro-demo-nested-drawer-2-close", "astro-demo-nested-drawer-2", "close"],
    [
      "astro-demo-nested-drawer-3-confirm",
      "astro-demo-nested-drawer-3",
      "close",
    ],
    [
      "astro-demo-nested-drawer-3-cancel",
      "astro-demo-nested-drawer-3",
      "close",
    ],
  ];

  triggers.forEach(([triggerId, dialogId, action]) => {
    document.getElementById(triggerId)?.addEventListener("click", () => {
      window.__PindobaDialogManager?.[action](dialogId);
    });
  });
</script>

Custom Styling

The passThrough prop provides escape hatches for advanced customization: style applies Panda CSS SystemStyleObject to any slot, and props forwards arbitrary HTML attributes.

Custom Styled Dialog

This dialog uses passThrough for dialog-level slots and bannerPassThrough to reach Banner internals: a narrower max-width, a colored header border, and a bolder heading.
---
import Dialog from "../dialog.astro";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { Icon } from "astro-icon/components";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md", direction: "column", align: "start" })}>
  <Dialog
    id="astro-demo-dialog-custom"
    title="Custom Styled Dialog"
    passThrough={{
      wrapper: { style: { maxWidth: "360px" } },
      header: {
        style: {
          borderBottom: "1px solid",
          borderColor: "primary.border",
        },
      },
      content: { style: { gap: "md" } },
    }}
    bannerPassThrough={{
      heading: { style: { fontWeight: "bold", fontSize: "lg" } },
    }}
  >
    <Stamp
      slot="heading-leading"
      shape="square"
      size="sm"
      translucent
      border="bold"
    >
      <Icon name="lucide:sparkles" />
    </Stamp>
    This dialog uses <code>passThrough</code> for dialog-level slots and
    <code>bannerPassThrough</code> to reach Banner internals: a narrower max-width,
    a colored header border, and a bolder heading.
  </Dialog>

  <Button id="astro-demo-dialog-custom-trigger">Open Custom Dialog</Button>
</div>

<script>
  document
    .getElementById("astro-demo-dialog-custom-trigger")
    ?.addEventListener("click", () => {
      window.__PindobaDialogManager?.open("astro-demo-dialog-custom");
    });
</script>

Props

props · 33 total
prop type default req description
open boolean false Controls the open/closed state of the dialog. Bindable in Svelte.
isModal boolean true When true, opens as a modal dialog using showModal() — blocks interaction with the rest of the page and enables the backdrop.
drawer "left""right""top""bottom" undefined Positions the dialog along an edge of the screen. Automatically removes the border radius on the attached edge unless overridden.
title string undefined Text displayed in the dialog header. When provided, the header is rendered with the title, close button, and any heading or subheading slots.
subtitle string undefined Secondary text displayed below the title. Renders the subheading row, which also accepts subheading-leading and subheading-trailing slots.
leading Snippetslot undefined Forwarded to the internal Banner's leading slot. Renders at the far-left of the header row, outside the heading/subheading group. (Svelte: Snippet / Astro: leading slot)
trailing Snippetslot undefined Forwarded to the internal Banner's trailing slot. Renders at the far-right of the header row, before the close button — the recommended home for custom header actions. (Svelte: Snippet / Astro: traili…

trailing

Forwarded to the internal Banner's trailing slot. Renders at the far-right of the header row, before the close button — the recommended home for custom header actions. (Svelte: Snippet / Astro: trailing slot)

Type Snippet | slot
Default undefined
Required No
headingLeading Snippetslot undefined Forwarded to Banner. Content placed to the left of the title inline with the heading row. (Svelte: Snippet / Astro: heading-leading slot)
headingTrailing Snippetslot undefined Forwarded to Banner. Content placed to the right of the title inline with the heading row. (Svelte: Snippet / Astro: heading-trailing slot)
subheadingLeading Snippetslot undefined Forwarded to Banner. Content placed to the left of the subtitle. Only rendered when subtitle is set. (Svelte: Snippet / Astro: subheading-leading slot)
subheadingTrailing Snippetslot undefined Forwarded to Banner. Content placed to the right of the subtitle. Only rendered when subtitle is set. (Svelte: Snippet / Astro: subheading-trailing slot)
bannerPassThrough BannerPassThrough undefined Forwarded to the internal Banner's passThrough. Use this to style heading/subheading slots (heading, subheading, headingLeading, etc.) that are now owned by Banner.
showCloseButton boolean true Whether to show the close button in the header.
lockScroll boolean true When true, prevents body scroll while the dialog is open. Uses a counter to handle stacked dialogs correctly.
excludeFromStack boolean false When true, this dialog does not participate in the dialog stack. Useful for popovers and non-blocking overlays. Auto-detected for popovers.
onChange (open: boolean) => void undefined Callback called when the dialog open state changes.
onOpen () => void undefined Callback called when the dialog opens.
onClose () => void undefined Callback called when the dialog closes.
background "surface""sunken""elevated""transparent" "surface" Background style of the dialog panel. surface: Default surface. sunken: Recessed. elevated: Raised with shadow. transparent: No background.
translucent boolean false Apply a frosted glass effect with backdrop blur to the dialog panel.
feedback "default""success""warning""danger" undefined Semantic feedback color scheme applied to the dialog panel.
padding "none""5xs""4xs""3xs""2xs""xs""sm""md""lg""xl""2xl""3xl""4xl""5xl""6xl""7xl""8xl""9xl""10xl""11xl" "none" Internal padding of the dialog panel.
radius "none""5xs""4xs""3xs""2xs""xs""sm""md""lg""xl""2xl""3xl""4xl""5xl""6xl""7xl""8xl""9xl""10xl""11xl""full" "md" Border radius of the dialog panel. Automatically set to none on the attached edge for drawer dialogs.
radiusTop SpacingScale undefined Border radius for top-left and top-right corners.
radiusBottom SpacingScale undefined Border radius for bottom-left and bottom-right corners.
radiusLeft SpacingScale undefined Border radius for top-left and bottom-left corners.
radiusRight SpacingScale undefined Border radius for top-right and bottom-right corners.
border "none""default""bold""muted" "default" Box-shadow border style of the dialog panel. none: No border. default: Standard border. bold: Stronger emphasis. muted: Subtle separation.
passThrough { root?: { style?: SystemStyleObject; props?: HTMLAttributes<HTMLDialogElement> }; wrapper?: { style?: SystemStyleObject; props?: HTMLAttributes<HTMLDivElement> }; header?: { style?: SystemStyleObject; props?: HTMLAttributes<HTMLDivElement> }; content?: { style?: SystemStyleObject; props?: HTMLAttributes<HTMLDivElement> }; closeButton?: { style?: SystemStyleObject; props?: Omit<ButtonProps, 'onclick'> } } undefined Custom styling and props for dialog-owned slots. Available slots: root, wrapper, header, content, closeButton. To style heading/subheading internals, use bannerPassThrough instead.
dialogElement HTMLDialogElement undefined Bindable reference to the underlying HTMLDialogElement. In Svelte, use bind:dialogElement to access the DOM node.
outsideContent Snippet undefined Snippet rendered outside the Panel wrapper but inside the dialog root. Useful for custom backdrops or overlapping elements. (Svelte only)
children Snippetslot undefined Dialog content rendered inside the content slot.
...rest HTMLAttributes<HTMLDialogElement> - Standard HTML dialog attributes forwarded to the root dialog element.