Dialog

PohonGitHub
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.
<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>

Features

  • Supports modal and non-modal modes.
  • Focus is automatically trapped when modal.
  • Can be controlled or uncontrolled.
  • Manages screen reader announcements with `` and `` components.
  • Esc closes the component automatically.

Anatomy

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>

Pohon

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.

API Reference

Root

Contains all the parts of a dialog

Props

Prop Default Type
defaultOpenfalseboolean

The open state of the dialog when it is initially rendered. Use when you do not need to control its open state.

modaltrueboolean

The modality of the dialog When set to true,
interaction with outside elements will be disabled and only dialog content will be visible to screen readers.

openboolean

Emits

Event Type
update:open[value: boolean]

Event handler called when the open state of the dialog changes.

Slots

Slot Type
openboolean

The controlled open state of the dialog. Can be binded as v-model:open.

close(): void

Close the dialog

Trigger

The button that opens the dialog

Props

Prop Default Type
as'button'APrimitiveAsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChildboolean

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 Attributes

Attribute Value
[data-state]'open' | 'closed'

Portal

When used, portals your overlay and content parts into the body.

Props

Prop Default Type
deferboolean

Defer 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}

disabledboolean

Disable teleport and render the component inline

{@link https://vuejs.org/guide/built-ins/teleport.html#disabling-teleport}

forceMountboolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

tostring | HTMLElement

Vue native teleport component prop :to

{@link https://vuejs.org/guide/built-ins/teleport.html#basic-usage}

Overlay

A layer that covers the inert portion of the view when the dialog is open.

Built with Presence component - supports any animation techniques while maintaining access to presence emitted events.

Props

Prop Default Type
as'div'APrimitiveAsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChildboolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

forceMountboolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

Data Attributes

Attribute Value
[data-state]'open' | 'closed'

Content

Contains content to be rendered in the open dialog

Built with Presence component - supports any animation techniques while maintaining access to presence emitted events.

Props

Prop Default Type
as'div'APrimitiveAsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChildboolean

Change the default rendered element for the one passed as a child, merging their props and behavior.

Read our Composition guide for more details.

disableOutsidePointerEventsboolean

When true, hover/focus/click interactions will be disabled on elements outside the DismissableLayer. Users will need to click twice on outside elements to interact with them: once to close the DismissableLayer, and again to trigger the element.

forceMountboolean

Used to force mounting when more control is needed. Useful when controlling animation with Vue animation libraries.

Emits

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]

Data Attributes

Attribute Value
[data-state]'open' | 'closed'

Close

The button that closes the dialog

Props

Prop Default Type
as'button'APrimitiveAsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChildboolean

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 <AVisuallyHidden asChild>.

Props

Prop Default Type
as'h2'APrimitiveAsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChildboolean

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 <AVisuallyHidden asChild>. If you want to remove the description entirely, remove this part and pass :aria-describedby="undefined" to ADialogContent.

Props

Prop Default Type
as'p'APrimitiveAsTag | Component

The element or component this component should render as. Can be overwritten by asChild.

asChildboolean

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.

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

Close after asynchronous form submission

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>

Scrollable overlay

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();
    }
  }"
>

Custom portal container

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>

Accessibility

Adheres to the ADialog WAI-ARIA design pattern.

Close icon button

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>

Close using slot props

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>

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

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 ADialogOverlay and ADialogClose parts.

Usage

<script setup>
import { ADialog, ADialogContent, ADialogTrigger } from './your-dialog';
</script>

<template>
  <ADialog>
    <ADialogTrigger>ADialog trigger</ADialogTrigger>
    <ADialogContent>ADialog Content</ADialogContent>
  </ADialog>
</template>

Implementation

// 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>