Dialog
Dialog is a modal window built on reka-ui's dialog primitive. It is controlled through v-model:visible, renders an overlay and a focus-trapped panel, and ships with header, body, and footer regions.
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Dialog } from '@myghf/ui'
const visible = ref(false)
</script>
<template>
<Dialog v-model:visible="visible" title="Publish changes" description="This updates the live site.">
<template #trigger>
<Button>Open dialog</Button>
</template>
<template #footer>
<Button variant="outline" @click="visible = false">Cancel</Button>
<Button @click="visible = false">Publish</Button>
</template>
</Dialog>
</template>Examples
Basic dialog
Pass title and description for the header, fill the default slot with body content, and use the footer slot for actions. The trigger slot is rendered as-child, so the element you provide becomes the dialog trigger.
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Dialog } from '@myghf/ui'
const visible = ref(false)
const published = ref(false)
function publish() {
visible.value = false
published.value = true
}
</script>
<template>
<!-- Dialog/Drawer portals need a browser, so keep the demo client-only. -->
<ClientOnly>
<Dialog
v-model:visible="visible"
title="Publish changes"
description="This will update the live site."
>
<p class="text-sm text-muted">You can keep editing after publishing.</p>
<template #footer>
<Button variant="outline" @click="visible = false">Cancel</Button>
<Button @click="publish()">Publish</Button>
</template>
<template #trigger>
<Button>Open dialog</Button>
</template>
</Dialog>
<p v-if="published" class="mt-3 text-sm text-muted">Published (demo only).</p>
<template #fallback>
<span class="text-sm text-muted">Loading dialog…</span>
</template>
</ClientOnly>
</template>Sizes
size sets the panel's maximum width. The panel is centred and capped at 85vh tall; its body scrolls while the header and footer stay fixed.
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Dialog } from '@myghf/ui'
type DialogSize = 'sm' | 'md' | 'lg' | 'xl'
const visible = ref(false)
const size = ref<DialogSize>('md')
const sizes: DialogSize[] = ['sm', 'md', 'lg', 'xl']
function open(next: DialogSize) {
size.value = next
visible.value = true
}
</script>
<template>
<ClientOnly>
<div class="flex flex-wrap gap-2">
<Button v-for="s in sizes" :key="s" variant="outline" @click="open(s)">
{{ s }}
</Button>
</div>
<Dialog
v-model:visible="visible"
:size="size"
:title="'Size: ' + size"
description="The panel max-width follows the size prop."
>
<p class="text-sm text-muted">
<code>sm</code> → <code>max-w-sm</code>, <code>md</code> → <code>max-w-md</code>,
<code>lg</code> → <code>max-w-2xl</code>, <code>xl</code> → <code>max-w-4xl</code>.
</p>
<template #footer>
<Button variant="outline" @click="visible = false">Close</Button>
</template>
</Dialog>
<template #fallback>
<span class="text-sm text-muted">Loading dialog…</span>
</template>
</ClientOnly>
</template>Props
| Prop | Type | Default | Description |
|---|---|---|---|
visible | boolean | false | Whether the dialog is open. Bind it with v-model:visible. |
title | string | — | Header title. Can also be provided through the header slot. |
description | string | — | Supporting line under the title. Can also be provided through the description slot. |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Panel max-width: sm → max-w-sm, md → max-w-md, lg → max-w-2xl, xl → max-w-4xl. |
Dialog is fully controlled: keep visible in your own state and let the component report requested changes through update:visible.
Events
| Event | Payload | Description |
|---|---|---|
update:visible | boolean | Emitted when the dialog requests to open or close (trigger click, overlay click, Esc, or the close button). |
close | — | Emitted once when the dialog closes. |
Slots
| Slot | Description |
|---|---|
trigger | Element that opens the dialog. Rendered through reka's as-child, so it must be a single element that accepts attributes and a ref. |
header | Replaces the title text. Rendered inside the <DialogTitle>. |
description | Replaces the description text. Rendered inside the <DialogDescription>. |
default | Body content, in the scrollable region. |
footer | Actions row, shown only when the slot is present. |
Exposed methods
None. Dialog does not call defineExpose. Open and close it by updating the bound visible value.
Accessibility
- reka-ui renders the panel with
role="dialog"andaria-modal="true", traps focus inside it, and restores focus to the trigger on close. - The title and description are wired to the panel through
aria-labelledby/aria-describedby. Provide at least atitle, or an accessible name via anaria-label. BecauseDialogonly renders<DialogTitle>whentitle(or theheaderslot) is present, a dialog with neither has no accessible name. - Esc closes the dialog and clicking the overlay dismisses it.
Dialogdoes not exposecloseOnEscape/closeOnOutsideprops; if you need different dismissal behaviour, use Drawer, which does. - The close button is a real
<button>witharia-label="Close". - Focus is trapped while the dialog is open, so packaging it inside a
<ClientOnly>is only about SSR hydration — it does not change the runtime behaviour.
Dark mode & RTL
- The overlay uses
bg-black/50 dark:bg-black/70, and the panel styles entirely from semantic tokens (bg-surface,border-border,text-foreground,text-muted,shadow-dialog), so both themes are covered. - The panel is centred with physical utilities (
left-1/2 -translate-x-1/2) because it is symmetric; no override is needed under RTL. Header and footer use flex withgapandjustify-end, and the header's close button sits at the end edge without physical padding.