<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog
    bind:open
    class="p-8"
  >
    {#snippet trigger(props)}
      <Button {...props}>Delete</Button>
    {/snippet}
    <div class="mb-4 flex items-start justify-between gap-4">
      <h2 class="text-2xl font-semibold">Modal Title</h2>
      <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (open = false)}>
        <X />
      </Button>
    </div>
    <p class="text-kumo-subtle">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
      labore et dolore magna aliqua.
    </p>
    <div class="mt-8 flex justify-end gap-2">
      <Button variant="secondary" onclick={() => (open = false)}>Cancel</Button>
      <Button variant="destructive" onclick={() => (open = false)}>Delete</Button>
    </div>
  </Dialog>
</div>

Installation

Barrel

import { Dialog } from 'kumo-svelte';

Granular

import { Dialog } from 'kumo-svelte/components/dialog';

Usage

<script lang="ts">
  import { Dialog, Button } from "kumo-svelte";

  let open = $state(false);
</script>

<Dialog bind:open title="Dialog Title" description="Dialog content goes here.">
  {#snippet trigger(props)}
    <Button {...props}>Open</Button>
  {/snippet}

  <div class="flex justify-end gap-2">
    <Button variant="secondary" onclick={() => (open = false)}>Cancel</Button>
  </div>
</Dialog>

Dialog vs Alert Dialog

The Dialog component supports two ARIA roles to properly convey semantic meaning to assistive technologies:

RoleUse CaseBehavior
role="dialog" (default)General-purpose modals, forms, content displayDismissible by default
role="alertdialog"Destructive actions, confirmations, critical warningsRequires explicit user acknowledgment

Examples

Basic Dialog

<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog
    bind:open
    class="p-8"
  >
    {#snippet trigger(props)}
      <Button {...props}>Click me</Button>
    {/snippet}
    <div class="mb-4 flex items-start justify-between gap-4">
      <h2 class="text-2xl font-semibold">Modal Title</h2>
      <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (open = false)}>
        <X />
      </Button>
    </div>
    <p class="text-kumo-subtle">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
      labore et dolore magna aliqua.
    </p>
  </Dialog>
</div>

Sizes

The size prop controls the fixed width of the dialog on desktop. Content that overflows the dialog width will scroll horizontally within the dialog rather than stretching it.

<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';

  const sizes = [
    { size: 'sm', label: 'Small', width: '288px' },
    { size: 'base', label: 'Base', width: '384px' },
    { size: 'lg', label: 'Large', width: '512px' },
    { size: 'xl', label: 'Extra Large', width: '768px' }
  ] as const;

  let openBySize = $state<Record<(typeof sizes)[number]['size'], boolean>>({
    sm: false,
    base: false,
    lg: false,
    xl: false
  });
</script>

<div class="flex flex-wrap gap-2">
  {#each sizes as { size, label, width } (size)}
    <Dialog bind:open={openBySize[size]} size={size} class="p-8">
      {#snippet trigger(props)}
        <Button variant="secondary" {...props}>{label} ({width})</Button>
      {/snippet}

      <div class="mb-4 flex items-start justify-between gap-4">
        <h2 class="text-2xl font-semibold">{label} Dialog</h2>
        <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (openBySize[size] = false)}>
          <X />
        </Button>
      </div>
      <p class="text-kumo-subtle">
        This <code>size="{size}"</code> dialog should stay at {width} wide regardless of the content below.
      </p>
      <div class="mt-4 overflow-auto rounded-md border border-kumo-line">
        <table class="w-max text-sm">
          <thead class="bg-kumo-elevated text-left">
            <tr>
              <th class="px-3 py-2">Resource</th>
              <th class="px-3 py-2">Region</th>
              <th class="px-3 py-2">Status</th>
              <th class="px-3 py-2">Latency</th>
              <th class="px-3 py-2">Requests</th>
              <th class="px-3 py-2">Last Deployed</th>
            </tr>
          </thead>
          <tbody class="divide-y divide-kumo-hairline">
            <tr>
              <td class="px-3 py-2">api-gateway-prod</td>
              <td class="px-3 py-2">us-east-1</td>
              <td class="px-3 py-2 text-kumo-success">Healthy</td>
              <td class="px-3 py-2">12ms</td>
              <td class="px-3 py-2">1,234,567</td>
              <td class="px-3 py-2">2026-06-23</td>
            </tr>
            <tr>
              <td class="px-3 py-2">worker-analytics</td>
              <td class="px-3 py-2">eu-west-1</td>
              <td class="px-3 py-2 text-kumo-warning">Degraded</td>
              <td class="px-3 py-2">89ms</td>
              <td class="px-3 py-2">456,789</td>
              <td class="px-3 py-2">2026-06-22</td>
            </tr>
          </tbody>
        </table>
      </div>
      <div class="mt-6 flex justify-end gap-2">
        <Button variant="secondary" onclick={() => (openBySize[size] = false)}>Close</Button>
      </div>
    </Dialog>
  {/each}
</div>

Alert Dialog (role="alertdialog")

For destructive or confirmation dialogs, use role="alertdialog" on Dialog. This provides proper accessibility semantics by rendering the dialog with role="alertdialog" instead of role="dialog".

When to use role="alertdialog":

  • Destructive actions (delete, discard, remove)
  • Confirmation flows requiring explicit user acknowledgment
  • Actions that cannot be undone
  • Critical warnings or errors
<script lang="ts">
  import Warning from 'phosphor-svelte/lib/Warning';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog bind:open role="alertdialog" class="p-8">
    {#snippet trigger(props)}
      <Button variant="destructive" {...props}>Delete Account</Button>
    {/snippet}
    <div class="mb-4 flex items-center gap-3">
      <div class="flex h-10 w-10 items-center justify-center rounded-full bg-kumo-danger/20">
        <Warning size={20} class="text-kumo-danger" weight="fill" />
      </div>
      <h2 class="text-xl font-semibold">Delete Account?</h2>
    </div>
    <p class="text-kumo-subtle">
      This action cannot be undone. All your data will be permanently removed from our servers.
      Are you sure you want to proceed?
    </p>
    <div class="mt-8 flex justify-end gap-2">
      <Button variant="secondary" onclick={() => (open = false)}>Cancel</Button>
      <Button variant="destructive" onclick={() => (open = false)}>Delete Account</Button>
    </div>
  </Dialog>
</div>

Confirmation Dialog (with disablePointerDismissal)

For confirmation dialogs that should not be dismissed by clicking outside, use disablePointerDismissal on Dialog. This can be combined with role="alertdialog" for proper accessibility.

<script lang="ts">
  import Warning from 'phosphor-svelte/lib/Warning';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog bind:open disablePointerDismissal class="p-8">
    {#snippet trigger(props)}
      <Button variant="destructive" {...props}>Delete Project</Button>
    {/snippet}
    <div class="mb-4 flex items-center gap-3">
      <div class="flex h-10 w-10 items-center justify-center rounded-full bg-kumo-danger/20">
        <Warning size={20} class="text-kumo-danger" />
      </div>
      <h2 class="text-xl font-semibold">Delete Project?</h2>
    </div>
    <p class="text-kumo-subtle">
      This action cannot be undone. This will permanently delete the project and all associated
      data.
    </p>
    <div class="mt-8 flex justify-end gap-2">
      <Button variant="secondary" onclick={() => (open = false)}>Cancel</Button>
      <Button variant="destructive" onclick={() => (open = false)}>Delete</Button>
    </div>
  </Dialog>
</div>

With Actions

<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog
    bind:open
    class="p-8"
  >
    {#snippet trigger(props)}
      <Button {...props}>Delete</Button>
    {/snippet}
    <div class="mb-4 flex items-start justify-between gap-4">
      <h2 class="text-2xl font-semibold">Modal Title</h2>
      <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (open = false)}>
        <X />
      </Button>
    </div>
    <p class="text-kumo-subtle">
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
      labore et dolore magna aliqua.
    </p>
    <div class="mt-8 flex justify-end gap-2">
      <Button variant="secondary" onclick={() => (open = false)}>Cancel</Button>
      <Button variant="destructive" onclick={() => (open = false)}>Delete</Button>
    </div>
  </Dialog>
</div>

Custom Max Width

Consumer max-width utilities such as max-w-lg should cap the dialog on desktop, even when the dialog contains wide intrinsic content.

<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog
    bind:open
    class="max-w-lg p-8"
  >
    {#snippet trigger(props)}
      <Button {...props}>Open capped dialog</Button>
    {/snippet}
    <div class="mb-4 flex items-start justify-between gap-4">
      <h2 class="text-2xl font-semibold">Max width override</h2>
      <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (open = false)}>
        <X />
      </Button>
    </div>
    <p class="text-kumo-subtle">
      This dialog uses <code>class="max-w-lg"</code> and should stay capped around 512px on desktop.
    </p>
    <div class="mt-4 truncate rounded-md border border-kumo-line bg-kumo-recessed p-3 font-mono text-sm">
      abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789
    </div>
  </Dialog>
</div>

With Select

Dialog containing a Select dropdown.

<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';
  import { Select } from 'kumo-svelte/components/select';

  const regions = [
    { value: 'us-east', label: 'US East' },
    { value: 'us-west', label: 'US West' },
    { value: 'eu-west', label: 'EU West' },
    { value: 'ap-south', label: 'AP South' }
  ];

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog bind:open class="p-8">
    {#snippet trigger(props)}
      <Button {...props}>Open Form</Button>
    {/snippet}
    <div class="mb-4 flex items-start justify-between gap-4">
      <h2 class="text-2xl font-semibold">Create Resource</h2>
      <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (open = false)}>
        <X />
      </Button>
    </div>
    <p class="mb-4 text-kumo-subtle">Select a region for your new resource.</p>
    <Select
      class="w-full"
      placeholder="Select region..."
      options={regions}
      renderValue={(value) => regions.find((region) => region.value === value)?.label}
    />
    <div class="mt-8 flex justify-end gap-2">
      <Button variant="secondary" onclick={() => (open = false)}>Cancel</Button>
      <Button variant="primary">Create</Button>
    </div>
  </Dialog>
</div>

With Combobox

Dialog containing a Combobox for searchable selection.

<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Combobox } from 'kumo-svelte/components/combobox';
  import { Dialog } from 'kumo-svelte/components/dialog';

  const regions = [
    { value: 'us-east', label: 'US East' },
    { value: 'us-west', label: 'US West' },
    { value: 'eu-west', label: 'EU West' },
    { value: 'ap-south', label: 'AP South' }
  ];

  let value = $state<(typeof regions)[number] | null>(null);
  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog
    bind:open
    class="p-8"
  >
    {#snippet trigger(props)}
      <Button {...props}>Open Form</Button>
    {/snippet}
    <div class="mb-4 flex items-start justify-between gap-4">
      <h2 class="text-2xl font-semibold">Create Resource</h2>
      <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (open = false)}>
        <X />
      </Button>
    </div>
    <p class="mb-4 text-kumo-subtle">Search and select a region for your new resource.</p>
    <Combobox bind:value items={regions}>
      <Combobox.TriggerInput class="w-full" placeholder="Search regions..." />
      <Combobox.Content>
        <Combobox.Empty>No regions found</Combobox.Empty>
        <Combobox.List>
          {#snippet children(item)}
            <Combobox.Item value={item}>{item.label}</Combobox.Item>
          {/snippet}
        </Combobox.List>
      </Combobox.Content>
    </Combobox>
    <div class="mt-8 flex justify-end gap-2">
      <Button variant="secondary" onclick={() => (open = false)}>Cancel</Button>
      <Button variant="primary">Create</Button>
    </div>
  </Dialog>
</div>

With Dropdown

Dialog containing a Dropdown menu.

<script lang="ts">
  import X from 'phosphor-svelte/lib/X';
  import { Button } from 'kumo-svelte/components/button';
  import { Dialog } from 'kumo-svelte/components/dialog';
  import { DropdownMenu } from 'kumo-svelte/components/dropdown-menu';

  let open = $state(false);
</script>

<div class="flex min-h-24 w-full items-center justify-center">
  <Dialog
    bind:open
    class="p-8"
  >
    {#snippet trigger(props)}
      <Button {...props}>Open Form</Button>
    {/snippet}
    <div class="mb-4 flex items-start justify-between gap-4">
      <h2 class="text-2xl font-semibold">Resource Actions</h2>
      <Button variant="secondary" shape="square" aria-label="Close" onclick={() => (open = false)}>
        <X />
      </Button>
    </div>
    <p class="mb-4 text-kumo-subtle">Choose an action for the selected resource.</p>
    <DropdownMenu>
      <DropdownMenu.Trigger>
        <Button>Actions</Button>
      </DropdownMenu.Trigger>
      <DropdownMenu.Content>
        <DropdownMenu.Item>Edit</DropdownMenu.Item>
        <DropdownMenu.Item>Duplicate</DropdownMenu.Item>
        <DropdownMenu.Separator />
        <DropdownMenu.Item variant="danger">Delete</DropdownMenu.Item>
      </DropdownMenu.Content>
    </DropdownMenu>
    <div class="mt-8 flex justify-end">
      <Button variant="secondary" onclick={() => (open = false)}>Close</Button>
    </div>
  </Dialog>
</div>

API Reference

Dialog

The main dialog container that renders the modal overlay and popup.

PropTypeDefaultDescription
role"dialog" | "alertdialog""dialog"The ARIA role for the dialog. Use "alertdialog" for destructive or confirmation flows.
disablePointerDismissalbooleanfalseWhen true, prevents the dialog from being dismissed by clicking outside.
PropTypeDefaultDescription
class string-Additional classes merged onto the dialog content.
children Snippet-Dialog content, typically title, description, close, and action buttons.
container HTMLElement | stringdocument.bodyPortal container for custom roots or Shadow DOM.
description string-Description rendered under the dialog title.
disablePointerDismissal booleanfalseWhen true, prevents the dialog from being dismissed by clicking outside.
open booleanfalseControlled open state. Supports bind:open.
role 'dialog' | 'alertdialog'"dialog"ARIA role for the dialog.
size 'sm' | 'base' | 'lg' | 'xl'"base"Fixed dialog width preset: sm (288px), base (384px), lg (512px), or xl (768px).
style string-Inline styles for the dialog content.
title string-Title rendered as the accessible dialog heading.
trigger Snippet<[Record<string, unknown>]>-Trigger snippet that receives props for opening the dialog.