<script setup lang="ts">
import {
ADialogClose,
ADialogContent,
ADialogDescription,
ADialogOverlay,
ADialogPortal,
ADialogRoot,
ADialogTitle,
ADialogTrigger,
} from 'akar';
</script>
<template>
<ADialogRoot>
<ADialogTrigger
class="text-sm color-primary font-500 px-2.5 py-1.5 rounded-md inline-flex gap-1.5 ring ring-primary/50 ring-inset shadow-md transition-colors-280 items-center focus:outline-none active:bg-primary/10 hover:bg-primary/10 focus-visible:(ring-2 ring-primary)"
>
Open dialog
</ADialogTrigger>
<ADialogPortal>
<ADialogOverlay class="bg-background-elevated/75 inset-0 fixed data-[state=closed]:(animate-out fade-out-0) data-[state=open]:(animate-in fade-in-0)" />
<ADialogContent
class="rounded-lg bg-background grid grid-rows-[min-content_1fr_min-content] max-h-[calc(100dvh-2rem)] max-w-lg w-[calc(100vw-2rem)] ring ring-ring shadow-lg left-1/2 top-1/2 fixed overflow-hidden divide-divide divide-y focus:outline-none sm:max-h-[calc(100dvh-4rem)] -translate-x-1/2 -translate-y-1/2 data-[state=closed]:(animate-out fade-out-0 zoom-out-95) data-[state=open]:(animate-in fade-in-0 zoom-in-95)"
>
<ADialogTitle class="color-text-highlighted font-semibold p-4 flex gap-1.5 min-h-16 items-center sm:px-6">
Edit profile
</ADialogTitle>
<ADialogDescription class="text-sm color-text-muted mt-1 p-4 flex-1 overflow-y-auto sm:p-6">
Make changes to your profile here. Click save when you're done.
</ADialogDescription>
<div class="p-4 flex gap-1.5 gap-4 items-center justify-end sm:px-6">
<ADialogClose as-child>
<button
class="text-sm color-text font-500 px-2.5 py-1.5 rounded-md bg-background-elevated inline-flex gap-1.5 shadow-md transition-colors-280 items-center focus:outline-none active:bg-background-accented/75 focus-visible:bg-background-accented/75 hover:bg-background-accented/75"
>
Save changes
</button>
</ADialogClose>
</div>
<ADialogClose
class="text-grass11 hover:bg-green4 focus:shadow-green7 appearance-none rounded-full inline-flex h-[25px] w-[25px] items-center right-[10px] top-[10px] justify-center absolute focus:outline-none focus:shadow-[0_0_0_2px]"
aria-label="Close"
>
<i class="i-lucide:x" />
</ADialogClose>
</ADialogContent>
</ADialogPortal>
</ADialogRoot>
</template>
Import all parts and piece them together.
<script setup>
import {
ADialogClose,
ADialogContent,
ADialogDescription,
ADialogOverlay,
ADialogPortal,
ADialogRoot,
ADialogTitle,
ADialogTrigger,
} from 'akar';
</script>
<template>
<ADialogRoot>
<ADialogTrigger />
<ADialogPortal>
<ADialogOverlay />
<ADialogContent>
<ADialogTitle />
<ADialogDescription />
<ADialogClose />
</ADialogContent>
</ADialogPortal>
</ADialogRoot>
</template>
One benefit of using Akar is its flexibility and low-level control over the components. However, this also means that you may need to manually construct more complex UI elements by combining multiple Akar components together.
If you feel there's a lot of elements that needs to be constructed manually using Akar, consider using Pohon UI instead. It provides a higher-level abstraction over Akar components with pre-defined styles and behaviors that can help you build UIs faster.
Contains all the parts of a dialog
| Prop | Default | Type |
|---|---|---|
defaultOpen | false | booleanThe open state of the dialog when it is initially rendered. Use when you do not need to control its open state. |
modal | true | booleanThe modality of the dialog When set to |
open | boolean |
| Event | Type |
|---|---|
update:open | [value: boolean]Event handler called when the open state of the dialog changes. |
| Slot | Type |
|---|---|
open | booleanThe controlled open state of the dialog. Can be binded as |
close | (): voidClose the dialog |
The button that opens the dialog
| Prop | Default | Type |
|---|---|---|
as | 'button' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
| Attribute | Value |
|---|---|
[data-state] | 'open' | 'closed' |
When used, portals your overlay and content parts into the body.
| Prop | Default | Type |
|---|---|---|
defer | booleanDefer the resolving of a Teleport target until other parts of the application have mounted (requires Vue 3.5.0+) {@link https://vuejs.org/guide/built-ins/teleport.html#deferred-teleport} | |
disabled | booleanDisable teleport and render the component inline {@link https://vuejs.org/guide/built-ins/teleport.html#disabling-teleport} | |
forceMount | booleanUsed to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. | |
to | string | HTMLElementVue native teleport component prop {@link https://vuejs.org/guide/built-ins/teleport.html#basic-usage} |
A layer that covers the inert portion of the view when the dialog is open.
| Prop | Default | Type |
|---|---|---|
as | 'div' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
forceMount | booleanUsed to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. |
| Attribute | Value |
|---|---|
[data-state] | 'open' | 'closed' |
Contains content to be rendered in the open dialog
| Prop | Default | Type |
|---|---|---|
as | 'div' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. | |
disableOutsidePointerEvents | booleanWhen | |
forceMount | booleanUsed to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries. |
| Event | Type |
|---|---|
closeAutoFocus | [event: Event]Event handler called when auto-focusing on close. Can be prevented. |
escapeKeyDown | [event: KeyboardEvent] |
focusOutside | [event: FocusOutsideEvent] |
interactOutside | [event: PointerDownOutsideEvent | FocusOutsideEvent] |
openAutoFocus | [event: Event]Event handler called when auto-focusing on open. Can be prevented. |
pointerDownOutside | [event: PointerDownOutsideEvent] |
| Attribute | Value |
|---|---|
[data-state] | 'open' | 'closed' |
The button that closes the dialog
| Prop | Default | Type |
|---|---|---|
as | 'button' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
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 <AVisuallyHidden asChild>.
| Prop | Default | Type |
|---|---|---|
as | 'h2' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
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 <AVisuallyHidden asChild>. If you want to remove the description entirely, remove this part and pass :aria-describedby="undefined" to ADialogContent.
| Prop | Default | Type |
|---|---|---|
as | 'p' | APrimitiveAsTag | ComponentThe element or component this component should render as. Can be overwritten by |
asChild | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. Read our Composition guide for more details. |
You can nest multiple layers of dialogs.
<script setup lang="ts">
import {
ADialogClose,
ADialogContent,
ADialogDescription,
ADialogOverlay,
ADialogPortal,
ADialogRoot,
ADialogTitle,
ADialogTrigger,
} from 'akar';
</script>
<template>
<div>
<ADialogRoot>
<ADialogTrigger
class="text-sm color-primary font-500 px-2.5 py-1.5 rounded-md inline-flex gap-1.5 ring ring-primary/50 ring-inset shadow-md transition-colors-280 items-center focus:outline-none active:bg-primary/10 hover:bg-primary/10 focus-visible:(ring-2 ring-primary)"
>
Open dialog
</ADialogTrigger>
<ADialogPortal>
<ADialogOverlay class="bg-background-elevated/75 inset-0 fixed data-[state=closed]:(animate-out fade-out-0) data-[state=open]:(animate-in fade-in-0)" />
<ADialogContent
class="rounded-lg bg-background grid grid-rows-[min-content_1fr_min-content] max-h-[calc(100dvh-2rem)] max-w-lg w-[calc(100vw-2rem)] ring ring-ring shadow-lg left-1/2 top-1/2 fixed overflow-hidden divide-divide divide-y focus:outline-none sm:max-h-[calc(100dvh-4rem)] -translate-x-1/2 -translate-y-1/2 data-[state=closed]:(animate-out fade-out-0 zoom-out-95) data-[state=open]:(animate-in fade-in-0 zoom-in-95)"
>
<ADialogTitle class="color-text-highlighted font-semibold p-4 flex gap-1.5 min-h-16 items-center sm:px-6">
First Dialog
</ADialogTitle>
<ADialogDescription class="text-sm color-text-muted mt-1 p-4 flex-1 overflow-y-auto sm:p-6">
First dialog.
</ADialogDescription>
<div class="p-4 flex gap-1.5 gap-4 items-center justify-end sm:px-6">
<ADialogClose as-child>
<button
class="text-sm color-text font-500 px-2.5 py-1.5 rounded-md bg-background-elevated inline-flex gap-1.5 shadow-md transition-colors-280 items-center focus:outline-none active:bg-background-accented/75 focus-visible:bg-background-accented/75 hover:bg-background-accented/75"
>
Close
</button>
</ADialogClose>
<ADialogRoot>
<ADialogTrigger
class="text-sm color-primary font-500 px-2.5 py-1.5 rounded-md inline-flex gap-1.5 ring ring-primary/50 ring-inset shadow-md transition-colors-280 items-center focus:outline-none active:bg-primary/10 hover:bg-primary/10 focus-visible:(ring-2 ring-primary)"
>
Open second
</ADialogTrigger>
<ADialogPortal>
<ADialogOverlay class="bg-background-elevated/75 inset-0 fixed data-[state=closed]:(animate-out fade-out-0) data-[state=open]:(animate-in fade-in-0)" />
<ADialogContent
class="rounded-lg bg-background grid grid-rows-[min-content_1fr_min-content] max-h-[calc(100dvh-2rem)] max-w-lg w-[calc(100vw-2rem)] ring ring-ring shadow-lg left-1/2 top-1/2 fixed overflow-hidden divide-divide divide-y focus:outline-none sm:max-h-[calc(100dvh-4rem)] -translate-x-1/2 -translate-y-1/2 data-[state=closed]:(animate-out fade-out-0 zoom-out-95) data-[state=open]:(animate-in fade-in-0 zoom-in-95)"
>
<ADialogTitle class="color-text-highlighted font-semibold p-4 flex gap-1.5 min-h-16 items-center sm:px-6">
Second Dialog
</ADialogTitle>
<ADialogDescription class="text-sm color-text-muted mt-1 p-4 flex-1 overflow-y-auto sm:p-6">
Second dialog.
</ADialogDescription>
<div class="p-4 flex gap-1.5 gap-4 items-center justify-end sm:px-6">
<ADialogClose as-child>
<button
class="text-sm color-text font-500 px-2.5 py-1.5 rounded-md bg-background-elevated inline-flex gap-1.5 shadow-md transition-colors-280 items-center focus:outline-none active:bg-background-accented/75 focus-visible:bg-background-accented/75 hover:bg-background-accented/75"
>
Close
</button>
</ADialogClose>
</div>
</ADialogContent>
</ADialogPortal>
</ADialogRoot>
</div>
<ADialogClose
class="text-grass11 hover:bg-green4 focus:shadow-green7 appearance-none rounded-full inline-flex h-[25px] w-[25px] items-center right-[10px] top-[10px] justify-center absolute focus:outline-none focus:shadow-[0_0_0_2px]"
aria-label="Close"
>
<i class="i-lucide:x" />
</ADialogClose>
</ADialogContent>
</ADialogPortal>
</ADialogRoot>
</div>
</template>
Use the controlled props to programmatically close the ADialog after an async operation has completed.
<script setup>
import { ADialogContent, ADialogOverlay, ADialogPortal, ADialogRoot, ADialogTrigger } from 'akar';
function wait() {
return new Promise((resolve) => {
setTimeout(resolve, 1000);
});
}
const open = ref(false);
</script>
<template>
<ADialogRoot v-model:open="open">
<ADialogTrigger>Open</ADialogTrigger>
<ADialogPortal>
<ADialogOverlay />
<ADialogContent>
<form
@submit.prevent="
(event) => {
wait().then(() => (open = false));
}
"
>
<!-- some inputs -->
<button type="submit">
Submit
</button>
</form>
</ADialogContent>
</ADialogPortal>
</ADialogRoot>
</template>
Move the content inside the overlay to render a dialog with overflow.
// index.vue
<script setup>
import { ADialogContent, ADialogOverlay, ADialogPortal, ADialogRoot, ADialogTrigger } from 'akar';
import './styles.css';
</script>
<template>
<ADialogRoot>
<ADialogTrigger />
<ADialogPortal>
<ADialogOverlay class="ADialogOverlay">
<ADialogContent class="ADialogContent">
...
</ADialogContent>
</ADialogOverlay>
</ADialogPortal>
</ADialogRoot>
</template>
/* styles.css */
.ADialogOverlay {
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;
}
.ADialogContent {
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 ADialogContent to prevent closing of modal when clicking on scrollbar.
<ADialogContent
@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();
}
}"
>
Customise the element that your dialog portals into.
<script setup>
import { ADialogContent, ADialogOverlay, ADialogPortal, ADialogRoot, ADialogTrigger } from 'akar';
const container = ref(null);
</script>
<template>
<div>
<ADialogRoot>
<ADialogTrigger />
<ADialogPortal to="container">
<ADialogOverlay />
<ADialogContent>...</ADialogContent>
</ADialogPortal>
</ADialogRoot>
<div ref="container" />
</div>
</template>
Adheres to the ADialog WAI-ARIA design pattern.
When providing an icon (or font icon), remember to label it correctly for screen reader users.
<template>
<ADialogRoot>
<ADialogTrigger />
<ADialogPortal>
<ADialogOverlay />
<ADialogContent>
<ADialogTitle />
<ADialogDescription />
<ADialogClose aria-label="Close">
<span aria-hidden="true">×</span>
</ADialogClose>
</ADialogContent>
</ADialogPortal>
</ADialogRoot>
</template>
Alternatively, you can use the close method provided by the ADialogRoot slot props to programmatically close the dialog.
<script setup>
import { ADialogContent, ADialogOverlay, ADialogPortal, ADialogRoot, ADialogTrigger } from 'akar';
</script>
<template>
<ADialogRoot v-slot="{ close }">
<ADialogTrigger>Open</ADialogTrigger>
<ADialogPortal>
<ADialogOverlay />
<ADialogContent>
<form>
<!-- some inputs -->
<button
type="submit"
@click="close"
>
Submit
</button>
</form>
</ADialogContent>
<ADialogFooter>
<button
type="submit"
@click="close"
>
Submit
</button>
</ADialogFooter>
</ADialogPortal>
</ADialogRoot>
</template>
| 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 |
Create your own API by abstracting the primitive parts into your own component.
This example abstracts the ADialogOverlay and ADialogClose parts.
<script setup>
import { ADialog, ADialogContent, ADialogTrigger } from './your-dialog';
</script>
<template>
<ADialog>
<ADialogTrigger>ADialog trigger</ADialogTrigger>
<ADialogContent>ADialog Content</ADialogContent>
</ADialog>
</template>
// your-dialog.ts
export { default as ADialogContent } from 'ADialogContent.vue';
export { ADialogRoot as ADialog, ADialogTrigger } from 'akar';
<!-- ADialogContent.vue -->
<script setup lang="ts">
import type { ADialogContentEmits, ADialogContentProps } from 'akar';
import { Cross2Icon } from '@radix-icons/vue';
import { ADialogClose, ADialogContent, ADialogOverlay, ADialogPortal, useForwardPropsEmits } from 'akar';
const props = defineProps<ADialogContentProps>();
const emits = defineEmits<ADialogContentEmits>();
const forwarded = useForwardPropsEmits(props, emits);
</script>
<template>
<ADialogPortal>
<ADialogOverlay />
<ADialogContent v-bind="forwarded">
<slot />
<ADialogClose>
<Cross2Icon />
<span class="sr-only">Close</span>
</ADialogClose>
</ADialogContent>
</ADialogPortal>
</template>