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.
A centered modal dialog with title, content, and a close button.
---
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.
---
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>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.
---
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>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.
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.
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.
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>The passThrough prop provides escape hatches for advanced customization: style applies Panda CSS SystemStyleObject to any slot, and props forwards arbitrary HTML attributes.
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>| 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…
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 Default Required | |
| 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. |