# Dialog

> A modal panel with a title, description, body and free-form footer — portals out, traps focus, and locks the page's scroll while open.

- Package: `fancy-ui-svelte` (Svelte 5 + Tailwind CSS v4)
- Category: Overlays
- Tags: modal, dialog, overlay, portal, focus-trap
- Docs: https://fancy-ui.rama.app/docs/components/dialog
- Source: https://github.com/RamaHerbin/fancy-ui/tree/main/src/lib/fancy-ui/dialog

## Import

```svelte
<script lang="ts">
  import { Dialog } from 'fancy-ui-svelte';
</script>
```

Setup (once per project): `npm install fancy-ui-svelte`, then add `@import "fancy-ui-svelte/tailwind.css";` after `@import "tailwindcss";` in your main CSS file. Full guide: https://fancy-ui.rama.app/llms.txt

React: `import { Dialog } from 'fancy-ui-react';` (same prop names; `class` becomes `className`, snippets become `ReactNode`).

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `open` | `boolean` | `false` | Whether the dialog is open; bindable |
| `onOpenChange` | `(open: boolean) => void` | — | Fires whenever open changes, from any trigger |
| `title` | `string` | — | The heading. Omitted entirely (not just visually) when not given |
| `description` | `string` | — | The copy under the title. Same omission rule as title |
| `dismissible` | `boolean` | `true` | Whether Escape and an outside click close the dialog; the close button always works |
| `initialFocus` | `HTMLElement \| null` | — | Element to focus once the dialog opens; defaults to the first focusable descendant |
| `class` | `string` | — | Additional CSS classes for the panel |
| `ref` | `HTMLDivElement \| null` | `null` | Bindable reference to the panel element |
| `sound` | `boolean` | `false` | Plays open when the trigger opens the dialog and close on every dismissal, once the user has enabled sound |

## Snippets

| Snippet | Description |
| --- | --- |
| `children` | The dialog's body |
| `footer` | The action row under the body — free-form, callers build their own buttons |
| `trigger` | Optional trigger; renders in place and opens the dialog on activation |

## Examples

### Basic Usage

A trigger, title, description, body and footer.

```svelte
<script lang="ts">
	import { Dialog } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

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

{#snippet trigger()}
	<Button>Invite member</Button>
{/snippet}

{#snippet footer()}
	<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
	<Button onclick={() => (open = false)}>Invite</Button>
{/snippet}

<Dialog
	sound
	bind:open
	{trigger}
	{footer}
	title="Invite a member"
	description="Send an email invite to join the workspace."
>
	<input
		type="email"
		bind:value={email}
		placeholder="email@example.com"
		aria-label="Email address"
		class="border-input bg-background text-muted-foreground placeholder:text-muted-foreground focus-visible:ring-ring/50 focus-visible:border-ring w-full rounded-lg border px-3 py-2 text-[12px] outline-none focus-visible:ring-2"
	/>
</Dialog>
```

### Controlled Open

Non-bound open plus onOpenChange, no trigger snippet.

```svelte
<script lang="ts">
	import { Dialog } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	// `open` is a plain, non-bound value here — the parent owns the state and
	// only ever learns about changes through `onOpenChange`, the same pattern
	// a caller reaches for when open/close is driven by something other than
	// this exact button (a route change, a keyboard shortcut, a wizard step).
	let isOpen = $state(false);
</script>

<Button onclick={() => (isOpen = true)}>Open dialog</Button>

<Dialog
	sound
	open={isOpen}
	onOpenChange={(next) => (isOpen = next)}
	title="Restart required"
	description="Applying this setting restarts the current session."
>
	{#snippet footer()}
		<Button variant="outline" onclick={() => (isOpen = false)}>Not now</Button>
		<Button onclick={() => (isOpen = false)}>Restart</Button>
	{/snippet}
</Dialog>
```

### Not Dismissible

dismissible={false} — Escape and outside click both disabled.

```svelte
<script lang="ts">
	import { Dialog } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

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

{#snippet trigger()}
	<Button variant="outline">Start setup</Button>
{/snippet}

<!--
	dismissible={false} turns off Escape and outside click — for a step that
	should not be lost to a stray click. The ✕ button is unaffected: it is
	an explicit action, not the accidental dismiss this prop guards against,
	so it stays the one deliberate way out.
-->
<Dialog
	sound
	bind:open
	{trigger}
	dismissible={false}
	title="Finish setting up your workspace"
	description="This only takes a moment. Escape and clicking outside are disabled here — use the buttons below or the close icon."
>
	{#snippet footer()}
		<Button onclick={() => (open = false)}>Done</Button>
	{/snippet}
</Dialog>
```

### Custom Initial Focus

Sends focus to a specific field instead of the default close button.

```svelte
<script lang="ts">
	import { Dialog } from "fancy-ui-svelte";
	import { Button } from "fancy-ui-svelte";

	let open = $state(false);
	let name = $state("Untitled project");
	let nameInput: HTMLInputElement | null = $state(null);
</script>

{#snippet trigger()}
	<Button variant="outline">Rename project</Button>
{/snippet}

<!--
	Without initialFocus, the first focusable descendant would be the ✕ close
	button — a reasonable default, but not what a rename dialog wants. Passing
	the input's own element sends focus straight to it instead.
-->
<Dialog sound bind:open initialFocus={nameInput} {trigger} title="Rename project">
	<input
		bind:this={nameInput}
		bind:value={name}
		type="text"
		class="border-input bg-background focus-visible:ring-ring/50 focus-visible:border-ring w-full rounded-lg border px-3 py-2 text-[13px] outline-none focus-visible:ring-2"
	/>
	{#snippet footer()}
		<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
		<Button onclick={() => (open = false)}>Save</Button>
	{/snippet}
</Dialog>
```
