Skip to content
Componentsbeta

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.

vue
<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.

Salsa Night is in your plan.
vue
<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.

vue
<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:

PropTypeDefaultDescription
openboolean—Dialog (root). Controlled open state — use with v-model:open. Forwarded to reka-ui DialogRoot.
defaultOpenbooleanfalseDialog (root). Initial state when uncontrolled.
modalbooleantrueDialog (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-childbooleanfalseDialogTrigger / DialogClose. Render your own Button instead of a bare <button>.
classHTMLAttributes["class"]—DialogContent, DialogScrollContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription. Merged with cn().
…reka attrsDialogContentProps—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 to aria-labelledby; without it screen readers announce an unnamed dialog. Add DialogDescription for aria-describedby — or pass :aria-describedby="undefined" to DialogContent if 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-child with a real <Button>, so the trigger is a native button with aria-expanded and aria-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.