Dialog
Features
- Supports modal and non-modal modes.
- Focus is automatically trapped when modal.
- Can be controlled or uncontrolled.
- Manages screen reader announcements with
Title
andDescription
components. - Esc closes the component automatically.
Installation
Install the component from your command line.
$ npm add reka-ui
Anatomy
Import all parts and piece them together.
<script setup>
import {
DialogClose,
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
DialogTrigger,
} from 'reka-ui'
</script>
<template>
<DialogRoot>
<DialogTrigger />
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle />
<DialogDescription />
<DialogClose />
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
API Reference
Root
Contains all the parts of a dialog
Prop | Default | Type |
---|---|---|
defaultOpen | false | boolean The open state of the dialog when it is initially rendered. Use when you do not need to control its open state. |
modal | true | boolean The modality of the dialog When set to |
open | boolean The controlled open state of the dialog. Can be binded as |
Emit | Payload |
---|---|
update:open | [value: boolean] Event handler called when the open state of the dialog changes. |
Slots (default) | Payload |
---|---|
open | boolean Current open state |
Trigger
The button that opens the dialog
Prop | Default | Type |
---|---|---|
as | 'button' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
Portal
When used, portals your overlay and content parts into the body
.
Prop | Default | Type |
---|---|---|
disabled | boolean Disable teleport and render the component inline | |
forceMount | boolean Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. | |
to | string | HTMLElement Vue native teleport component prop |
Overlay
A layer that covers the inert portion of the view when the dialog is open.
Prop | Default | Type |
---|---|---|
as | 'div' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
forceMount | boolean Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
Content
Contains content to be rendered in the open dialog
Prop | Default | Type |
---|---|---|
as | 'div' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
disableOutsidePointerEvents | boolean When | |
forceMount | boolean Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. | |
trapFocus | boolean When |
Emit | Payload |
---|---|
closeAutoFocus | [event: Event] Event handler called when auto-focusing on close. Can be prevented. |
escapeKeyDown | [event: KeyboardEvent] Event handler called when the escape key is down. Can be prevented. |
focusOutside | [event: FocusOutsideEvent] Event handler called when the focus moves outside of the |
interactOutside | [event: PointerDownOutsideEvent | FocusOutsideEvent] Event handler called when an interaction happens outside the |
openAutoFocus | [event: Event] Event handler called when auto-focusing on open. Can be prevented. |
pointerDownOutside | [event: PointerDownOutsideEvent] Event handler called when the a |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
Close
The button that closes the dialog
Prop | Default | Type |
---|---|---|
as | 'button' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
Title
An accessible title to be announced when the dialog is opened.
If you want to hide the title, wrap it inside our Visually Hidden utility like this <VisuallyHidden asChild>
.
Prop | Default | Type |
---|---|---|
as | 'h2' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
Description
An optional accessible description to be announced when the dialog is opened.
If you want to hide the description, wrap it inside our Visually Hidden utility like this <VisuallyHidden asChild>
. If you want to remove the description entirely, remove this part and pass :aria-describedby="undefined"
to DialogContent
.
Prop | Default | Type |
---|---|---|
as | 'p' | AsTag | Component The element or component this component should render as. Can be overwrite by |
asChild | boolean Change the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
Examples
Nested dialog
You can nest multiple layers of dialogs.
Close after asynchronous form submission
Use the controlled props to programmatically close the Dialog after an async operation has completed.
<script setup>
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui'
const wait = () => new Promise(resolve => setTimeout(resolve, 1000))
const open = ref(false)
</script>
<template>
<DialogRoot v-model:open="open">
<DialogTrigger>Open</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<form
@submit.prevent="
(event) => {
wait().then(() => (open = false));
}
"
>
<!-- some inputs -->
<button type="submit">
Submit
</button>
</form>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>
Scrollable overlay
Move the content inside the overlay to render a dialog with overflow.
// index.vue
<script setup>
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui'
import './styles.css'
</script>
<template>
<DialogRoot>
<DialogTrigger />
<DialogPortal>
<DialogOverlay class="DialogOverlay">
<DialogContent class="DialogContent">
...
</DialogContent>
</DialogOverlay>
</DialogPortal>
</DialogRoot>
</template>
/* styles.css */
.DialogOverlay {
background: rgba(0 0 0 / 0.5);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: grid;
place-items: center;
overflow-y: auto;
}
.DialogContent {
min-width: 300px;
background: white;
padding: 30px;
border-radius: 4px;
}
However, there's a caveat to this approach, where user might click on the scrollbar and close the dialog unintentionally. There's no universal solution that would fix this issue for now, however you can add the following snippet to DialogContent
to prevent closing of modal when clicking on scrollbar.
<DialogContent
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
Custom portal container
Customise the element that your dialog portals into.
<script setup>
import { DialogContent, DialogOverlay, DialogPortal, DialogRoot, DialogTrigger } from 'reka-ui'
const container = ref(null)
</script>
<template>
<div>
<DialogRoot>
<DialogTrigger />
<DialogPortal to="container">
<DialogOverlay />
<DialogContent>...</DialogContent>
</DialogPortal>
</DialogRoot>
<div ref="container" />
</div>
</template>
Disable close on Interaction outside
For example, if you have some global Toaster component that should not close the Dialog when clicking on it.
Accessibility
Adheres to the Dialog WAI-ARIA design pattern.
Close icon button
When providing an icon (or font icon), remember to label it correctly for screen reader users.
<DialogRoot>
<DialogTrigger />
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle />
<DialogDescription />
<DialogClose aria-label="Close">
<span aria-hidden="true">×</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</DialogRoot>
Keyboard Interactions
Key | Description |
---|---|
Space | Opens/closes the dialog |
Enter | Opens/closes the dialog |
Tab | Moves focus to the next focusable element. |
Shift + Tab | Moves focus to the previous focusable element. |
Esc | Closes the dialog and moves focus to DialogTrigger . |
Custom APIs
Create your own API by abstracting the primitive parts into your own component.
Abstract the overlay and the close button
This example abstracts the DialogOverlay
and DialogClose
parts.
Usage
<script setup>
import { Dialog, DialogContent, DialogTrigger } from './your-dialog'
</script>
<template>
<Dialog>
<DialogTrigger>Dialog trigger</DialogTrigger>
<DialogContent>Dialog Content</DialogContent>
</Dialog>
</template>
Implementation
// your-dialog.ts
export { default as DialogContent } from 'DialogContent.vue'
export { DialogRoot as Dialog, DialogTrigger } from 'reka-ui'
<!-- DialogContent.vue -->
<script setup lang="ts">
import { DialogClose, DialogContent, type DialogContentEmits, type DialogContentProps, DialogOverlay, DialogPortal, useEmitAsProps, } from 'reka-ui'
import { Cross2Icon } from '@radix-icons/vue'
const props = defineProps<DialogContentProps>()
const emits = defineEmits<DialogContentEmits>()
const emitsAsProps = useEmitAsProps(emits)
</script>
<template>
<DialogPortal>
<DialogOverlay />
<DialogContent v-bind="{ ...props, ...emitsAsProps }">
<slot />
<DialogClose>
<Cross2Icon />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>