Dialog
Dialog is a thin wrapper over Ark UI dialog that shows a modal window on top of the page. It is modal by default — focus is trapped, scrolling is blocked, and outside interaction is prevented. DialogBackdrop and DialogContent each portal internally; the content uses the existing --background tokens.
State is controlled from outside: keep open in your own component state and pass it with onOpenChange. There is no DialogTrigger requirement — a plain <button> (or a menu item) outside the dialog opens it by setting open to true.
Edit Profile
// apps/docs/src/components/examples/dialog/default/react.tsx
import { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogDescription,
DialogDismiss,
DialogContent,
DialogBackdrop,
} from '@cloudvoyant/helical-react';
import { X } from 'lucide-react';
export default function ReactDialogDefault() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogDismiss>
<X />
</DialogDismiss>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>Make changes to your profile here. Click save when you're done.</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/default/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogDismiss, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
import { X } from 'lucide-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent>
<DialogDismiss>
<X />
</DialogDismiss>
<DialogTitle>Edit Profile</DialogTitle>
<DialogDescription>Make changes to your profile here. Click save when you're done.</DialogDescription>
</DialogContent>
</Dialog>
</div>Examples
Alert Dialog
Are you absolutely sure?
// apps/docs/src/components/examples/dialog/alert-dialog/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogAlertDialog() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground"
>
Delete account
</button>
<Dialog role="alertdialog" open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>
This action cannot be undone. This will permanently delete your account and remove your data.
</DialogDescription>
<div className="flex justify-end gap-2">
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
onClick={() => setOpen(false)}
>
Cancel
</button>
<button
type="button"
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground"
onClick={() => setOpen(false)}
>
Delete
</button>
</div>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/alert-dialog/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground"
onclick={() => (open = true)}
>
Delete account
</button>
<Dialog role="alertdialog" bind:open>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Are you absolutely sure?</DialogTitle>
<DialogDescription>This action cannot be undone. This will permanently delete your account and remove your data.</DialogDescription>
<div class="flex justify-end gap-2">
<button type="button" class="rounded-md border px-3 py-1.5 text-sm font-medium" onclick={() => (open = false)}>
Cancel
</button>
<button
type="button"
class="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground"
onclick={() => (open = false)}
>
Delete
</button>
</div>
</DialogContent>
</Dialog>
</div>Confirmation
Unsaved changes
// apps/docs/src/components/examples/dialog/confirmation/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogConfirmation() {
const [dirty, setDirty] = useState(true);
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog
open={open}
onOpenChange={(e) => setOpen(e.open)}
onEscapeKeyDown={(e) => {
if (dirty) e.preventDefault();
}}
onInteractOutside={(e) => {
if (dirty) e.preventDefault();
}}
>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Unsaved changes</DialogTitle>
<DialogDescription>You have unsaved changes that will be lost. Leave anyway?</DialogDescription>
<div className="flex justify-end gap-2">
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
onClick={() => setOpen(false)}
>
Cancel
</button>
<button
type="button"
className="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground"
onClick={() => {
setDirty(false);
setOpen(false);
}}
>
Discard
</button>
</div>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/confirmation/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let dirty = $state(true);
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog
bind:open
onEscapeKeyDown={(e) => {
if (dirty) e.preventDefault();
}}
onInteractOutside={(e) => {
if (dirty) e.preventDefault();
}}
>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Unsaved changes</DialogTitle>
<DialogDescription>You have unsaved changes that will be lost. Leave anyway?</DialogDescription>
<div class="flex justify-end gap-2">
<button type="button" class="rounded-md border px-3 py-1.5 text-sm font-medium" onclick={() => (open = false)}>
Cancel
</button>
<button
type="button"
class="rounded-md bg-destructive px-3 py-1.5 text-sm font-medium text-destructive-foreground"
onclick={() => {
dirty = false;
open = false;
}}
>
Discard
</button>
</div>
</DialogContent>
</Dialog>
</div>Context
Dialog is closed
Context
Dialog is closed
// apps/docs/src/components/examples/dialog/context/react.tsx
import { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogDescription,
DialogContent,
DialogBackdrop,
useDialogContext,
} from '@cloudvoyant/helical-react';
function DialogStatus() {
const dialog = useDialogContext();
return <p className="text-sm text-muted-foreground">Dialog is {dialog.open ? 'open' : 'closed'}</p>;
}
export default function ReactDialogContext() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogStatus />
<DialogBackdrop />
<DialogContent>
<DialogTitle>Context</DialogTitle>
<DialogDescription>The open state above is read via useDialogContext.</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/context/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop, DialogContext } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open>
<DialogContext>
{#snippet children(dialog)}
<p class="text-sm text-muted-foreground">Dialog is {dialog().open ? 'open' : 'closed'}</p>
{/snippet}
</DialogContext>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Context</DialogTitle>
<DialogDescription>The open state above is read via the dialog context.</DialogDescription>
</DialogContent>
</Dialog>
</div>Controlled
Controlled Dialog
// apps/docs/src/components/examples/dialog/controlled/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogControlled() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Controlled Dialog</DialogTitle>
<DialogDescription>This dialog is fully controlled by the checkbox.</DialogDescription>
</DialogContent>
</Dialog>
<label className="flex items-center gap-2 text-sm text-muted-foreground">
<input type="checkbox" checked={open} onChange={(e) => setOpen(e.target.checked)} />
Open
</label>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/controlled/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Controlled Dialog</DialogTitle>
<DialogDescription>This dialog is fully controlled by the checkbox.</DialogDescription>
</DialogContent>
</Dialog>
<label class="flex items-center gap-2 text-sm text-muted-foreground">
<input type="checkbox" bind:checked={open} />
Open
</label>
</div>Dialog With Tooltip
Dialog with Tooltip
// apps/docs/src/components/examples/dialog/dialog-with-tooltip/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
import { Tooltip, TooltipTrigger, TooltipContent } from '@cloudvoyant/helical-react';
export default function ReactDialogWithTooltip() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
</TooltipTrigger>
<TooltipContent>Shows on hover over the trigger</TooltipContent>
</Tooltip>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Dialog with Tooltip</DialogTitle>
<DialogDescription>The trigger also shows a tooltip on hover.</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/dialog-with-tooltip/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
import { Tooltip, TooltipTrigger, TooltipContent } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<Tooltip>
<TooltipTrigger>
{#snippet asChild(propsFn)}
<button
{...propsFn()}
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
{/snippet}
</TooltipTrigger>
<TooltipContent>Shows on hover over the trigger</TooltipContent>
</Tooltip>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Dialog with Tooltip</DialogTitle>
<DialogDescription>The trigger also shows a tooltip on hover.</DialogDescription>
</DialogContent>
</Dialog>
</div>Final Focus
Final Focus
// apps/docs/src/components/examples/dialog/final-focus/react.tsx
import { useRef, useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogFinalFocus() {
const [open, setOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)} finalFocusEl={() => buttonRef.current}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Final Focus</DialogTitle>
<DialogDescription>Focus returns to the outlined button when this dialog closes.</DialogDescription>
</DialogContent>
</Dialog>
<button ref={buttonRef} type="button" className="rounded-md border px-3 py-1.5 text-sm font-medium">
Receive focus on close
</button>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/final-focus/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
let buttonEl: HTMLButtonElement | undefined = $state();
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open finalFocusEl={() => buttonEl}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Final Focus</DialogTitle>
<DialogDescription>Focus returns to the outlined button when this dialog closes.</DialogDescription>
</DialogContent>
</Dialog>
<button type="button" bind:this={buttonEl} class="rounded-md border px-3 py-1.5 text-sm font-medium">
Receive focus on close
</button>
</div>Fullscreen
Fullscreen
// apps/docs/src/components/examples/dialog/fullscreen/react.tsx
import { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogDescription,
DialogHeader,
DialogFooter,
DialogDismiss,
DialogContent,
DialogBackdrop,
} from '@cloudvoyant/helical-react';
import { Scroll } from '@cloudvoyant/helical-react';
import { X } from 'lucide-react';
export default function ReactDialogFullscreen() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Fullscreen
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent fullscreen className="flex flex-col gap-0">
<DialogHeader sticky className="border-b px-6 py-4">
<div className="flex items-center justify-between">
<DialogTitle>Fullscreen</DialogTitle>
<DialogDismiss>
<X />
</DialogDismiss>
</div>
<DialogDescription>This dialog fills the whole viewport, edge to edge.</DialogDescription>
</DialogHeader>
<Scroll className="min-h-0 flex-1">
<div className="p-6 text-sm text-muted-foreground">
{Array.from({ length: 40 }, (_, i) => (
<p key={i} className="mb-2">
Clause {i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
))}
</div>
</Scroll>
<DialogFooter sticky className="border-t px-6 py-4">
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
onClick={() => setOpen(false)}
>
Cancel
</button>
<button
type="button"
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onClick={() => setOpen(false)}
>
Save
</button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/fullscreen/svelte.svelte -->
<script lang="ts">
import {
Dialog,
DialogTitle,
DialogDescription,
DialogHeader,
DialogFooter,
DialogDismiss,
DialogContent,
DialogBackdrop,
} from '@cloudvoyant/helical-svelte';
import { Scroll } from '@cloudvoyant/helical-svelte';
import { X } from 'lucide-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Fullscreen
</button>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent fullscreen class="flex flex-col gap-0">
<DialogHeader sticky class="border-b px-6 py-4">
<div class="flex items-center justify-between">
<DialogTitle>Fullscreen</DialogTitle>
<DialogDismiss>
<X />
</DialogDismiss>
</div>
<DialogDescription>This dialog fills the whole viewport, edge to edge.</DialogDescription>
</DialogHeader>
<Scroll class="min-h-0 flex-1">
<div class="p-6 text-sm text-muted-foreground">
{#each Array.from({ length: 40 }, (_, i) => i) as i}
<p class="mb-2">Clause {i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
{/each}
</div>
</Scroll>
<DialogFooter sticky class="border-t px-6 py-4">
<button type="button" class="rounded-md border px-3 py-1.5 text-sm font-medium" onclick={() => (open = false)}>
Cancel
</button>
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = false)}
>
Save
</button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>Initial Focus
Edit Name
// apps/docs/src/components/examples/dialog/initial-focus/react.tsx
import { useRef, useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogInitialFocus() {
const [open, setOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)} initialFocusEl={() => inputRef.current}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Edit Name</DialogTitle>
<DialogDescription>The input below receives focus when the dialog opens.</DialogDescription>
<input
ref={inputRef}
className="mt-2 h-9 rounded-md border border-input bg-transparent px-2.5 text-sm focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30"
placeholder="Enter your name"
/>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/initial-focus/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
let inputEl: HTMLInputElement | undefined = $state();
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open initialFocusEl={() => inputEl}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Edit Name</DialogTitle>
<DialogDescription>The input below receives focus when the dialog opens.</DialogDescription>
<input
bind:this={inputEl}
class="mt-2 h-9 rounded-md border border-input bg-transparent px-2.5 text-sm focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/30"
placeholder="Enter your name"
/>
</DialogContent>
</Dialog>
</div>Inside Scroll
Terms and Conditions
// apps/docs/src/components/examples/dialog/inside-scroll/react.tsx
import { useState } from 'react';
import {
Dialog,
DialogTitle,
DialogDescription,
DialogHeader,
DialogFooter,
DialogContent,
DialogBackdrop,
} from '@cloudvoyant/helical-react';
import { Scroll } from '@cloudvoyant/helical-react';
export default function ReactDialogInsideScroll() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent className="flex max-h-[420px] flex-col gap-0 overflow-hidden p-0">
<DialogHeader sticky className="px-6 pt-6 pb-4">
<DialogTitle>Terms and Conditions</DialogTitle>
<DialogDescription>Scroll inside the dialog while the header and footer stay put.</DialogDescription>
</DialogHeader>
<Scroll className="min-h-0 flex-1">
<div className="p-6 text-sm text-muted-foreground">
{Array.from({ length: 40 }, (_, i) => (
<p key={i} className="mb-2">
Clause {i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
))}
</div>
</Scroll>
<DialogFooter sticky className="px-6 py-4">
<button
type="button"
className="rounded-md border px-3 py-1.5 text-sm font-medium"
onClick={() => setOpen(false)}
>
Decline
</button>
<button
type="button"
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onClick={() => setOpen(false)}
>
Accept
</button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/inside-scroll/svelte.svelte -->
<script lang="ts">
import {
Dialog,
DialogTitle,
DialogDescription,
DialogHeader,
DialogFooter,
DialogContent,
DialogBackdrop,
} from '@cloudvoyant/helical-svelte';
import { Scroll } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent class="flex max-h-[420px] flex-col gap-0 overflow-hidden p-0">
<DialogHeader sticky class="px-6 pt-6 pb-4">
<DialogTitle>Terms and Conditions</DialogTitle>
<DialogDescription>Scroll inside the dialog while the header and footer stay put.</DialogDescription>
</DialogHeader>
<Scroll class="min-h-0 flex-1">
<div class="p-6 text-sm text-muted-foreground">
{#each Array.from({ length: 40 }, (_, i) => i) as i}
<p class="mb-2">Clause {i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit.</p>
{/each}
</div>
</Scroll>
<DialogFooter sticky class="px-6 py-4">
<button type="button" class="rounded-md border px-3 py-1.5 text-sm font-medium" onclick={() => (open = false)}>
Decline
</button>
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = false)}
>
Accept
</button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>Lazy Mount
// apps/docs/src/components/examples/dialog/lazy-mount/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogLazyMount() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)} lazyMount unmountOnExit>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Lazy Mounted</DialogTitle>
<DialogDescription>This dialog mounts only when first opened and unmounts when closed.</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/lazy-mount/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open lazyMount unmountOnExit>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Lazy Mounted</DialogTitle>
<DialogDescription>This dialog mounts only when first opened and unmounts when closed.</DialogDescription>
</DialogContent>
</Dialog>
</div>Multiple Triggers
General
// apps/docs/src/components/examples/dialog/multiple-triggers/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
const sections = [
{ id: 'general', title: 'General', body: 'General account settings.' },
{ id: 'privacy', title: 'Privacy', body: 'Privacy and data settings.' },
{ id: 'billing', title: 'Billing', body: 'Billing and payment settings.' },
];
export default function ReactDialogMultipleTriggers() {
const [open, setOpen] = useState(false);
const [active, setActive] = useState(sections[0]);
return (
<div className="flex flex-col items-center gap-4">
<div className="flex items-center gap-2">
{sections.map((section) => (
<button
key={section.id}
type="button"
onClick={() => {
setActive(section);
setOpen(true);
}}
className="rounded-md bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground"
>
{section.title}
</button>
))}
</div>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>{active.title}</DialogTitle>
<DialogDescription>{active.body}</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/multiple-triggers/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
const sections = [
{ id: 'general', title: 'General', body: 'General account settings.' },
{ id: 'privacy', title: 'Privacy', body: 'Privacy and data settings.' },
{ id: 'billing', title: 'Billing', body: 'Billing and payment settings.' },
];
let open = $state(false);
let active = $state(sections[0]);
</script>
<div class="flex flex-col items-center gap-4">
<div class="flex items-center gap-2">
{#each sections as section (section.id)}
<button
type="button"
class="rounded-md bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground"
onclick={() => {
active = section;
open = true;
}}
>
{section.title}
</button>
{/each}
</div>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent>
<DialogTitle>{active.title}</DialogTitle>
<DialogDescription>{active.body}</DialogDescription>
</DialogContent>
</Dialog>
</div>Nested
Outer Dialog
Inner Dialog
// apps/docs/src/components/examples/dialog/nested/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogNested() {
const [open, setOpen] = useState(false);
const [nestedOpen, setNestedOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Outer Dialog</DialogTitle>
<DialogDescription>Dialogs can be nested inside one another.</DialogDescription>
<button
type="button"
onClick={() => setNestedOpen(true)}
className="mt-2 rounded-md bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground"
>
Open Nested Dialog
</button>
</DialogContent>
</Dialog>
<Dialog open={nestedOpen} onOpenChange={(e) => setNestedOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Inner Dialog</DialogTitle>
<DialogDescription>This dialog sits on top of the outer one.</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/nested/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
let nestedOpen = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Outer Dialog</DialogTitle>
<DialogDescription>Dialogs can be nested inside one another.</DialogDescription>
<button
type="button"
class="mt-2 rounded-md bg-secondary px-3 py-1.5 text-sm font-medium text-secondary-foreground"
onclick={() => (nestedOpen = true)}
>
Open Nested Dialog
</button>
</DialogContent>
</Dialog>
<Dialog bind:nestedOpen>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Inner Dialog</DialogTitle>
<DialogDescription>This dialog sits on top of the outer one.</DialogDescription>
</DialogContent>
</Dialog>
</div>Non Modal
Non-Modal Dialog
// apps/docs/src/components/examples/dialog/non-modal/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent } from '@cloudvoyant/helical-react';
export default function ReactDialogNonModal() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)} modal={false}>
<DialogContent>
<DialogTitle>Non-Modal Dialog</DialogTitle>
<DialogDescription>You can interact with the page behind this dialog.</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/non-modal/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open modal={false}>
<DialogContent>
<DialogTitle>Non-Modal Dialog</DialogTitle>
<DialogDescription>You can interact with the page behind this dialog.</DialogDescription>
</DialogContent>
</Dialog>
</div>Open From Menu
Opened Externally
Outside Scroll
Extends Beyond Viewport
Section 1: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 2: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 3: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 4: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 5: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 6: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 7: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 8: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 9: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 10: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 11: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 12: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 13: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 14: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 15: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 16: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 17: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 18: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 19: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 20: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 21: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 22: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 23: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 24: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 25: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 26: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 27: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 28: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 29: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
Section 30: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
// apps/docs/src/components/examples/dialog/outside-scroll/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogOutsideScroll() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent positionerClassName="overflow-y-auto">
<DialogTitle>Extends Beyond Viewport</DialogTitle>
<DialogDescription>The positioner scrolls so the dialog can exceed the viewport height.</DialogDescription>
<div className="text-sm text-muted-foreground">
{Array.from({ length: 30 }, (_, i) => (
<p key={i} className="mb-2">
Section {i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.
</p>
))}
</div>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/outside-scroll/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent positionerClass="overflow-y-auto">
<DialogTitle>Extends Beyond Viewport</DialogTitle>
<DialogDescription>The positioner scrolls so the dialog can exceed the viewport height.</DialogDescription>
<div class="text-sm text-muted-foreground">
{#each Array.from({ length: 30 }, (_, i) => i) as i}
<p class="mb-2">Section {i + 1}: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor.</p>
{/each}
</div>
</DialogContent>
</Dialog>
</div>Root Provider
Dialog is closed
Controlled Externally
Dialog is closed
// apps/docs/src/components/examples/dialog/root-provider/react.tsx
import { useState } from 'react';
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-react';
export default function ReactDialogRootProvider() {
const [open, setOpen] = useState(false);
return (
<div className="flex flex-col items-center gap-4">
<p className="text-sm text-muted-foreground">Dialog is {open ? 'open' : 'closed'}</p>
<button
type="button"
onClick={() => setOpen(true)}
className="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
>
Open Dialog
</button>
<Dialog open={open} onOpenChange={(e) => setOpen(e.open)}>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Controlled Externally</DialogTitle>
<DialogDescription>This dialog is controlled from a plain button outside.</DialogDescription>
</DialogContent>
</Dialog>
</div>
);
}<!-- apps/docs/src/components/examples/dialog/root-provider/svelte.svelte -->
<script lang="ts">
import { Dialog, DialogTitle, DialogDescription, DialogContent, DialogBackdrop } from '@cloudvoyant/helical-svelte';
let open = $state(false);
</script>
<div class="flex flex-col items-center gap-4">
<p class="text-sm text-muted-foreground">Dialog is {open ? 'open' : 'closed'}</p>
<button
type="button"
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground"
onclick={() => (open = true)}
>
Open Dialog
</button>
<Dialog bind:open>
<DialogBackdrop />
<DialogContent>
<DialogTitle>Controlled Externally</DialogTitle>
<DialogDescription>This dialog is controlled from a plain button outside.</DialogDescription>
</DialogContent>
</Dialog>
</div>API Reference
Dialog
Props
| Prop | Type | Default | Description |
|---|---|---|---|
open / onOpenChange |
boolean / (details) => void |
— | Controlled open state. |
defaultOpen |
boolean |
false |
Initial open state when uncontrolled. |
modal |
boolean |
true |
Prevent pointer interaction outside and hide content below it. |
role |
'dialog' | 'alertdialog' |
'dialog' |
The dialog’s role. |
closeOnEscape |
boolean |
true |
Close when Escape is pressed. |
closeOnInteractOutside |
boolean |
true |
Close on outside click. |
initialFocusEl |
() => MaybeElement |
— | Element to focus when opened. |
finalFocusEl |
() => MaybeElement |
— | Element to focus when closed (defaults to the trigger). |
lazyMount / unmountOnExit |
boolean |
false |
Mount/unmount content only around open/close. |
trapFocus / preventScroll |
boolean |
true |
Focus trapping and scroll lock. |
Accepts all Ark DialogRootProps.
DialogProvider
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value |
UseDialogReturn |
— | The API object from useDialog(). |
DialogTrigger
Accepts all Ark DialogTriggerProps plus className/class. Supports the value prop for multiple triggers.
DialogBackdrop
Accepts all Ark DialogBackdropProps plus className/class. Portalled internally; rendered as a translucent black layer.
DialogContent
Props
| Prop | Type | Default | Description |
|---|---|---|---|
positionerClassName / positionerClass |
string |
— | Class applied to the internal positioner (e.g. overflow-y-auto for the Outside Scroll example). |
fullscreen |
boolean |
false |
Fill the whole viewport edge to edge (no border, padding, or rounding). |
Accepts all Ark DialogContentProps plus className/class.
DialogTitle
Accepts all Ark DialogTitleProps plus className/class.
DialogDescription
Accepts all Ark DialogDescriptionProps plus className/class.
DialogHeader
A flex flex-col gap-2 wrapper for the title and description. Pass sticky to pin it to the top of a scrolling dialog (sticky top-0 z-10 bg-background).
DialogFooter
A flex flex-col-reverse gap-2 sm:flex-row sm:justify-end wrapper for action buttons. Pass sticky to pin it to the bottom of a scrolling dialog (sticky bottom-0 z-10 bg-background).
DialogDismiss
Accepts all Ark DialogCloseTriggerProps plus className/class. Ships with aria-label="Close".
Hooks
useDialog— Ark machine factory; returnsUseDialogReturnforDialogProvider.useDialogContext— reads the current dialog state from context.
Accessibility
Complies with the Dialog WAI-ARIA design pattern. Keyboard support: Enter opens from the trigger; Tab/Shift+Tab move focus within the trapped dialog; Esc closes and returns focus to the trigger or finalFocusEl.
Theme extensions
None. The dialog reuses existing tokens (--background, --foreground, --border, --ring, --muted) and a translucent black backdrop (bg-black/50) — no theme.css changes.