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!
---
// 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.

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.
---
// 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>

Custom header

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>

Card

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.
  • The close button sits in the header’s trailing slot, always last: after card.trailing, then the dialog’s own trailing.
  • The children are the card content.
  • A footer is new: 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:

Rotate production API key

These services still use the current key.

  • billing-worker4 minutes ago
  • webhooks-relay1 hour ago
  • nightly-exportyesterday, 02:00 UTC
The old key stays valid for 24 hours.
---
// 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.wrapper styles the card root, passThrough.content the card content and passThrough.header the header Banner. The header’s bottom border is now the card’s divided hairline. Footer actions you used to place at the end of the body belong in the new footer slots.

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.
---
// 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>

Sizes

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.

Small

400px wide

A short confirmation or a single field.

Medium

540px wide, the default

The default size, for most forms and messages.

Large

720px wide

Room for a two-column form or a short table.

Extra large

960px wide

Wide content such as a data table or a side-by-side comparison.

report-q3.pdf

Covers the viewport, minus a gutter on every side

The file preview fills the rest of the height.
---
// 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

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.

Edit profile

Full screen on phones, a regular dialog above 640px

Resize the window below 640px (or open this on a phone): the dialog fills the viewport with a dynamic-viewport height and square corners.

Full screen

This dialog fills the viewport at every width.
---
// 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>

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.

---
// 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>

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.
---
// 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>

Accessibility

  • Name and description. When the dialog renders its own 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).
  • Close button label. The close button is announced as 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.
  • Components
  • Blocks