<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:
| Role | Use Case | Behavior |
|---|---|---|
role="dialog" (default) | General-purpose modals, forms, content display | Dismissible by default |
role="alertdialog" | Destructive actions, confirmations, critical warnings | Requires 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| role | "dialog" | "alertdialog" | "dialog" | The ARIA role for the dialog. Use "alertdialog" for destructive or
confirmation flows. |
| disablePointerDismissal | boolean | false | When true, prevents the dialog from being dismissed by clicking outside. |
| Prop | Type | Default | Description |
|---|---|---|---|
| class | string | - | Additional classes merged onto the dialog content. |
| children | Snippet | - | Dialog content, typically title, description, close, and action buttons. |
| container | HTMLElement | string | document.body | Portal container for custom roots or Shadow DOM. |
| description | string | - | Description rendered under the dialog title. |
| disablePointerDismissal | boolean | false | When true, prevents the dialog from being dismissed by clicking outside. |
| open | boolean | false | Controlled 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. |