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.
---
// GENERATED from packages/demos/dialog/default.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { Info } from "@lucide/astro";
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"
tone="primary"
emphasis="secondary"
>
<Info size={12} />
</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-default-trigger">Open Dialog</Button>
</div>
<script>
document
.getElementById("astro-demo-dialog-default-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.
---
// GENERATED from packages/demos/dialog/header.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Folder, User, Bell, Funnel } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md", direction: "row", flexWrap: "wrap" })}>
<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>
<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.ground"
size="sm"
>
<Folder size={14} />
</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>
<Dialog
id="astro-demo-dialog-header-account"
title="Account Settings"
subtitle="Manage your preferences and security"
>
<Stamp
slot="heading-leading"
tone="primary"
emphasis="secondary"
shape="circle"
size="sm"
>
<User size={14} />
</Stamp>
<Badge slot="subheading-trailing" size="sm" tone="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>
<Dialog
id="astro-demo-dialog-header-notifications"
title="Notifications"
subtitle="3 unread messages"
>
<Stamp
slot="heading-leading"
tone="warning"
emphasis="secondary"
shape="circle"
size="sm"
>
<Bell size={14} />
</Stamp>
<Badge slot="heading-trailing" size="sm" tone="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>
<Dialog id="astro-demo-dialog-header-filters" title="Filters">
<Stamp
slot="heading-leading"
tone="primary"
emphasis="secondary"
shape="square"
size="sm"
>
<Funnel size={14} />
</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>
document
.getElementById("astro-demo-dialog-header-notice-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-header-notice");
});
document
.getElementById("astro-demo-dialog-header-files-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-header-files");
});
document
.getElementById("astro-demo-dialog-header-account-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-header-account");
});
document
.getElementById("astro-demo-dialog-header-notifications-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open(
"astro-demo-dialog-header-notifications",
);
});
document
.getElementById("astro-demo-dialog-header-filters-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-header-filters");
});
</script>For headers that aren’t a title/subtitle — a search field, a segmented control, a stepper — the header snippet (Astro: header slot) replaces the Banner with your own content while keeping the header row’s styling and the close button. It renders even when no title/subtitle is set, which makes it the foundation for command-palette-style dialogs.
---
// GENERATED from packages/demos/dialog/header-input.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import { InputRoot, InputField } from "@pindoba/astro-input";
import Affix from "@pindoba/astro-affix";
import Stamp from "@pindoba/astro-stamp";
import { Search } from "@lucide/astro";
import { stack } from "@pindoba/styled-system/patterns";
const commands = [
"Open file…",
"Go to line",
"Toggle terminal",
"Format document",
"Rename symbol",
"Find in files",
];
const query = "";
const results = commands.filter((c) =>
c.toLowerCase().includes(query.toLowerCase()),
);
---
<div
class={stack({ gap: "md", direction: "row", flexWrap: "wrap" })}
data-ir-root="dialog/header-input"
>
<Dialog
id="astro-demo-dialog-header-input"
showCloseButton={false}
passThrough={{ header: { style: { p: "xs" } } }}
data-ir="1"
>
<InputRoot slot="header" fullWidth>
<Affix decorative>
<Stamp emphasis="ghost">
<Search size={16} />
</Stamp>
</Affix>
<InputField type="search" placeholder="Type a command…" data-ir="6" />
</InputRoot>
<div class={stack({ gap: "3xs", p: "xs" })}>
<!--ir:2-->
{results.map((result) => (
<Button
emphasis="ghost"
passThrough={{
root: {
style: { width: "100%", justifyContent: "flex-start" },
},
}}
data-ir-key={result}
>
<Fragment set:html="<!--ir:3-->" />
{result}
<Fragment set:html="<!--/ir-->" />
</Button>
))}
<!--/ir-->
<!--ir:4-->
{results.length === 0 && (
<span>
No commands match “<!--ir:5-->
{query}
<!--/ir-->”.
</span>
)}
<!--/ir-->
</div>
</Dialog>
<Button id="astro-demo-dialog-header-input-trigger" data-ir="7">
Command Palette
</Button>
</div>
<script>
import { hydrate } from "./_client/header-input.ts";
document
.querySelectorAll('[data-ir-root="dialog/header-input"]')
.forEach((root) => hydrate([root]));
</script>The dialog is card-based: the <dialog> element keeps its own surface, positioning and animation, and inside it the dialog renders a Card. By default that card is invisible — transparent, no border or shadow, corners following the dialog’s — and divided, so hairlines separate the header, body and footer.
title and subtitle become the card heading and subheading, keeping the ids the <dialog>’s aria-labelledby / aria-describedby point at.card.trailing, then the dialog’s own trailing.footer, footerLeading and footerTrailing — Svelte snippets, React props, Vue slots (#footer, #footerLeading, #footerTrailing), Astro named slots (footer, footer-leading, footer-trailing).The card prop takes the Card props that make sense inside a dialog — contentBackground / content, divided, size, leading, footer, passThrough… The surface props that belong to the <dialog> itself (background, border, shadow, radius) stay on Dialog, and the heading comes from title / subtitle. Pair the dialog’s background with card.contentBackground for a shell with an inset body:
---
// GENERATED from packages/demos/dialog/inset.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import { css } from "@pindoba/styled-system/css";
import { hstack, stack } from "@pindoba/styled-system/patterns";
const services = [
{ name: "billing-worker", lastUsed: "4 minutes ago" },
{ name: "webhooks-relay", lastUsed: "1 hour ago" },
{ name: "nightly-export", lastUsed: "yesterday, 02:00 UTC" },
];
const list = css({ listStyle: "none", margin: "0", padding: "0" });
const row = css({
display: "flex",
justifyContent: "space-between",
gap: "sm",
paddingBlock: "2xs",
});
const muted = css({ color: "panel.text.muted" });
---
<div class={stack({ gap: "md", direction: "column", align: "start" })}>
<Dialog
id="astro-demo-dialog-inset"
title="Rotate production API key"
subtitle="These services still use the current key."
background="surface.base"
card={{ contentBackground: "surface.peak" }}
>
<span slot="footer-leading" class={muted}>
The old key stays valid for 24 hours.
</span>
<div slot="footer-trailing" class={hstack({ gap: "xs" })}>
<Button id="astro-demo-dialog-inset-trigger" emphasis="ghost">
Cancel
</Button>
<Button id="astro-demo-dialog-inset-trigger-2">Rotate key</Button>
</div>
<ul class={list}>
{services.map((service) => (
<li class={row}>
<span>{service.name}</span>
<span class={muted}>{service.lastUsed}</span>
</li>
))}
</ul>
</Dialog>
<Button id="astro-demo-dialog-inset-trigger-3">Rotate key…</Button>
</div>
<script>
document
.getElementById("astro-demo-dialog-inset-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.close("astro-demo-dialog-inset");
});
document
.getElementById("astro-demo-dialog-inset-trigger-2")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.close("astro-demo-dialog-inset");
});
document
.getElementById("astro-demo-dialog-inset-trigger-3")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-inset");
});
</script>card={false} renders the pre-card bare wrapper. Popovers built on Dialog (data-popover — Popover, Tooltip, Menu, Select, Combobox) render bare unless they are handed a card object, so their DOM is unchanged.
<Dialog title="Settings" card={false} bind:open>…</Dialog>
Migrating from the pre-card dialog: the wrapper
<div>inside the<dialog>is now a Card ([data-component="card"]) —passThrough.wrapperstyles the card root,passThrough.contentthe card content andpassThrough.headerthe header Banner. The header’s bottom border is now the card’sdividedhairline. Footer actions you used to place at the end of the body belong in the new footer slots.
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.
---
// GENERATED from packages/demos/dialog/drawer.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { PanelRight, PanelLeft, PanelTop, PanelBottom } from "@lucide/astro";
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"
tone="primary"
emphasis="secondary"
>
<PanelRight size={14} />
</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"
tone="primary"
emphasis="secondary"
>
<PanelLeft size={14} />
</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"
tone="primary"
emphasis="secondary"
>
<PanelTop size={14} />
</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"
tone="primary"
emphasis="secondary"
>
<PanelBottom size={14} />
</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>
document
.getElementById("astro-demo-drawer-right-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-drawer-right");
});
document
.getElementById("astro-demo-drawer-left-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-drawer-left");
});
document
.getElementById("astro-demo-drawer-top-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-drawer-top");
});
document
.getElementById("astro-demo-drawer-bottom-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-drawer-bottom");
});
</script>size picks the dialog’s width from a preset: "sm" (400px), "md" (540px, the default), "lg" (720px) or "xl" (960px). A preset never gets wider than the viewport minus a gutter on each side (16px on phones, 24px from 768px up), so it never overflows a small screen.
size="viewport" covers almost the whole screen. It keeps that gutter on every side, so the backdrop and the page stay visible around it, and it keeps its rounded corners. Its height is fixed, so the body can fill it: a child with flex: 1 or height: 100% stretches to the full height. The body scrolls on its own while the header and footer stay put. Use it for file viewers, previews and editors that need the room.
size combines with fullscreen, and fullscreen wins below its breakpoint. size="viewport" fullscreen="sm" is full screen on phones and a near-full-viewport dialog above 640px. A left or right drawer uses "sm", "lg" and "xl" as its width. Top and bottom drawers already span the screen, so they ignore size, and so does "viewport" on any drawer; both log a warning in development.
---
// GENERATED from packages/demos/dialog/sizes.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
import { css } from "@pindoba/styled-system/css";
---
<div class={stack({ gap: "sm", direction: "row", flexWrap: "wrap" })}>
<Dialog
id="astro-demo-dialog-size-sm"
title="Small"
subtitle="400px wide"
size="sm"
>
A short confirmation or a single field.
</Dialog>
<Dialog
id="astro-demo-dialog-size-md"
title="Medium"
subtitle="540px wide, the default"
size="md"
>
The default size, for most forms and messages.
</Dialog>
<Dialog
id="astro-demo-dialog-size-lg"
title="Large"
subtitle="720px wide"
size="lg"
>
Room for a two-column form or a short table.
</Dialog>
<Dialog
id="astro-demo-dialog-size-xl"
title="Extra large"
subtitle="960px wide"
size="xl"
>
Wide content such as a data table or a side-by-side comparison.
</Dialog>
<Dialog
id="astro-demo-dialog-size-viewport"
title="report-q3.pdf"
subtitle="Covers the viewport, minus a gutter on every side"
size="viewport"
>
<div
class={css({
flex: "1",
display: "grid",
placeItems: "center",
minHeight: "0",
borderRadius: "md",
borderWidth: "1px",
borderStyle: "dashed",
borderColor: "neutral.border.muted",
background: "neutral.surface.ground",
})}
>
The file preview fills the rest of the height.
</div>
</Dialog>
<Button id="astro-demo-dialog-size-sm-trigger" emphasis="secondary">
Small
</Button>
<Button id="astro-demo-dialog-size-md-trigger" emphasis="secondary">
Medium
</Button>
<Button id="astro-demo-dialog-size-lg-trigger" emphasis="secondary">
Large
</Button>
<Button id="astro-demo-dialog-size-xl-trigger" emphasis="secondary">
Extra large
</Button>
<Button id="astro-demo-dialog-size-viewport-trigger">Viewport</Button>
</div>
<script>
document
.getElementById("astro-demo-dialog-size-sm-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-size-sm");
});
document
.getElementById("astro-demo-dialog-size-md-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-size-md");
});
document
.getElementById("astro-demo-dialog-size-lg-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-size-lg");
});
document
.getElementById("astro-demo-dialog-size-xl-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-size-xl");
});
document
.getElementById("astro-demo-dialog-size-viewport-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-size-viewport");
});
</script>fullscreen makes the dialog fill the viewport: full width, a 100dvh height (it tracks the mobile browser’s collapsing URL bar), no margin and square corners. Pass true to go full screen at every width, or a breakpoint ("sm", "md" or "lg") to go full screen only below it. fullscreen="sm" gives you a full-screen sheet on phones (under 640px) and a regular centered dialog everywhere else, so you no longer need a passThrough.root style override. It combines with drawer: the drawer keeps its slide-in animation, and fullscreen replaces its size and inset.
---
// GENERATED from packages/demos/dialog/fullscreen.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "sm", direction: "row", align: "start" })}>
<Dialog
id="astro-demo-dialog-fullscreen-sm"
title="Edit profile"
subtitle="Full screen on phones, a regular dialog above 640px"
fullscreen="sm"
>
Resize the window below 640px (or open this on a phone): the dialog fills
the viewport with a dynamic-viewport height and square corners.
</Dialog>
<Dialog
id="astro-demo-dialog-fullscreen-always"
title="Full screen"
fullscreen
>
This dialog fills the viewport at every width.
</Dialog>
<Button id="astro-demo-dialog-fullscreen-sm-trigger">
Full screen on mobile
</Button>
<Button id="astro-demo-dialog-fullscreen-always-trigger" emphasis="secondary">
Always full screen
</Button>
</div>
<script>
document
.getElementById("astro-demo-dialog-fullscreen-sm-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-dialog-fullscreen-sm");
});
document
.getElementById("astro-demo-dialog-fullscreen-always-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open(
"astro-demo-dialog-fullscreen-always",
);
});
</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.
---
// GENERATED from packages/demos/dialog/nested.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import Badge from "@pindoba/astro-badge";
import Stamp from "@pindoba/astro-stamp";
import { Settings, User, CircleCheckBig } from "@lucide/astro";
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.muted", fontSize: "sm" })}>
Open multiple drawers to see the cascade effect. Each drawer slides inward
and dims when another opens on top.
</p>
<Button id="astro-demo-nested-drawer-1-trigger">Open First Drawer</Button>
<Dialog id="astro-demo-nested-drawer-1" title="Settings" drawer="right">
<Stamp slot="heading-leading" shape="circle" size="sm" emphasis="secondary">
<Settings size={14} />
</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 }).map((_, i) => (
<p class={css({ color: "neutral.text.muted" })}>
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"
tone="primary"
emphasis="secondary"
>
<User size={14} />
</Stamp>
<Badge slot="heading-trailing" size="sm" tone="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-trigger-2" emphasis="ghost">
Close This Drawer
</Button>
</div>
{Array.from({ length: 6 }).map((_, i) => (
<p class={css({ color: "neutral.text.muted" })}>
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"
tone="success"
emphasis="secondary"
>
<CircleCheckBig size={14} />
</Stamp>
<Badge slot="heading-trailing" size="sm" tone="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-trigger-2" emphasis="primary">
Confirm
</Button>
<Button id="astro-demo-nested-drawer-3-trigger-3" emphasis="ghost">
Cancel
</Button>
</div>
</div>
</Dialog>
</div>
<script>
document
.getElementById("astro-demo-nested-drawer-1-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-nested-drawer-1");
});
document
.getElementById("astro-demo-nested-drawer-2-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-nested-drawer-2");
});
document
.getElementById("astro-demo-nested-drawer-3-trigger")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.open("astro-demo-nested-drawer-3");
});
document
.getElementById("astro-demo-nested-drawer-2-trigger-2")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.close("astro-demo-nested-drawer-2");
});
document
.getElementById("astro-demo-nested-drawer-3-trigger-2")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.close("astro-demo-nested-drawer-3");
});
document
.getElementById("astro-demo-nested-drawer-3-trigger-3")
?.addEventListener("click", () => {
window.__PindobaDialogManager?.close("astro-demo-nested-drawer-3");
});
</script>The passThrough prop provides escape hatches for advanced customization: style applies Panda CSS SystemStyleObject to any slot, and props forwards arbitrary HTML attributes.
---
// GENERATED from packages/demos/dialog/custom.demo.tsx — edit the source, not this file.
import Dialog from "@pindoba/astro-dialog";
import Button from "@pindoba/astro-button";
import Stamp from "@pindoba/astro-stamp";
import { Sparkles } from "@lucide/astro";
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: {
// Longhands, not `borderBottom` + `borderColor`: those shorthands race
// the Panel's own `border-width: 0` / `border-color` atomics, and the
// winner is Panda's emit order. Longhands always sort after them.
borderBottomWidth: "1px",
borderBottomStyle: "solid",
borderBottomColor: "primary.border",
},
},
content: { style: { gap: "md" } },
}}
bannerPassThrough={{
heading: { style: { fontWeight: "bold", fontSize: "lg" } },
}}
>
<Stamp
slot="heading-leading"
shape="square"
size="sm"
translucent
border="bold"
>
<Sparkles size={14} />
</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>title, the <dialog> gets aria-labelledby pointing at the title heading, so screen readers announce it by name. A subtitle becomes its aria-describedby. If you set aria-label or aria-labelledby (or aria-describedby) yourself, directly or through passThrough.root.props, yours wins. A custom header replaces the title Banner, so in that case give the dialog an aria-label (or aria-labelledby pointing at your own heading).closeLabel, which defaults to "Close". Set it to localize the dialog (closeLabel="Fechar"). passThrough.closeButton.props["aria-label"] and closeButtonProps["aria-label"] still take precedence.