Dialog
A modal for one focused task — book a lesson, confirm a removal, sign up. It blocks the page, so use it only when the task can’t wait.
Demo
The standard WeDance modal: cream surface, red accent bar, amber eyebrow, Playfair title. DialogContent is full-bleed below 640px by default — cap it with max-w-[calc(100vw-2rem)] so it keeps a 16px gutter on phones.
<Dialog>
<DialogTrigger as-child>
<Button>Book a private lesson</Button>
</DialogTrigger>
<DialogContent class="max-w-[calc(100vw-2rem)] sm:max-w-md border-0 p-0 overflow-hidden rounded-2xl">
<div class="h-1.5 bg-primary" />
<div class="px-6 pb-6 pt-4">
<DialogHeader class="text-left">
<span class="text-[10px] uppercase tracking-[0.3em] font-bold text-secondary">Teacher</span>
<DialogTitle class="font-display font-bold text-2xl">Private lesson with Lisandra</DialogTitle>
<DialogDescription>60 minutes, Cuban salsa, in central Munich.</DialogDescription>
</DialogHeader>
<DialogFooter class="mt-6 gap-2">
<DialogClose as-child>
<Button variant="outline">Not now</Button>
</DialogClose>
<Button>Send request</Button>
</DialogFooter>
</div>
</DialogContent>
</Dialog>Controlled confirmation
Bind v-model:open when your code needs to close the dialog after an action.
<script setup lang="ts">
const open = ref(false)
function removeFromPlan() {
// …do the work, then close
open.value = false
}
</script>
<template>
<Dialog v-model:open="open">
<DialogTrigger as-child>
<Button variant="destructive">Remove from plan</Button>
</DialogTrigger>
<DialogContent class="max-w-[calc(100vw-2rem)] sm:max-w-sm rounded-2xl">
<DialogHeader>
<DialogTitle>Remove Salsa Night?</DialogTitle>
<DialogDescription>It disappears from your week. Friends you shared it with keep their copy.</DialogDescription>
</DialogHeader>
<DialogFooter class="gap-2">
<DialogClose as-child><Button variant="outline">Keep it</Button></DialogClose>
<Button variant="destructive" @click="removeFromPlan">Remove</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>Long content
DialogScrollContent scrolls the overlay instead of the panel — for line-ups, terms or anything taller than a phone screen.
<Dialog>
<DialogTrigger as-child>
<Button variant="outline">Festival line-up</Button>
</DialogTrigger>
<!-- Scrolls the whole overlay — use for content taller than the viewport -->
<DialogScrollContent class="max-w-[calc(100vw-2rem)] sm:max-w-lg rounded-2xl">
<DialogHeader>
<DialogTitle>Line-up</DialogTitle>
</DialogHeader>
…
</DialogScrollContent>
</Dialog>Props
Dialog is a set of parts over reka-ui. The props that matter, by part:
| Prop | Type | Default | Description |
|---|---|---|---|
| open | boolean | — | Dialog (root). Controlled open state — use with v-model:open. Forwarded to reka-ui DialogRoot. |
| defaultOpen | boolean | false | Dialog (root). Initial state when uncontrolled. |
| modal | boolean | true | Dialog (root). Traps focus and blocks the page behind. Leave on. |
| @update:open | (open: boolean) => void | — | Dialog (root). Fires on open and close, including Esc and overlay click. |
| as-child | boolean | false | DialogTrigger / DialogClose. Render your own Button instead of a bare <button>. |
| class | HTMLAttributes["class"] | — | DialogContent, DialogScrollContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription. Merged with cn(). |
| …reka attrs | DialogContentProps | — | DialogContent forwards attrs to reka-ui DialogContent, e.g. @escape-key-down, @interact-outside, @open-auto-focus. |
Do & don't
Do — Name the action in the buttons: “Remove” / “Keep it”. The dancer knows what happens without re-reading the title.
Don't — “OK” / “Cancel”. In a removal dialog, “Cancel” is ambiguous — cancel the removal, or cancel the event?
Accessibility
- Every dialog needs a
DialogTitle. reka-ui wires it toaria-labelledby; without it screen readers announce an unnamed dialog. AddDialogDescriptionforaria-describedby— or pass:aria-describedby="undefined"toDialogContentif there is genuinely nothing to describe. - Focus moves into the dialog on open and returns to the trigger on close. Focus is trapped while open, and Esc closes it. Don’t override these.
- The built-in close (×) has an sr-only “Close” label. Still give a visible text button (“Not now”, “Keep it”) — the × is small on phones.
- Use
DialogTrigger as-childwith a real<Button>, so the trigger is a native button witharia-expandedandaria-haspopup="dialog". - Destructive confirmations: put the safe option first in the DOM and make it the default focus target, so a stray Enter doesn’t delete anything.