Dialog

AkarGitHub
A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

Usage

Use a Button or any other component in the default slot of the Modal.

Then, use the #content slot to add the content displayed when the Modal is open.

<template>
  <PDialog>
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <CorePlaceholder class="h-48 m-4" />
    </template>
  </PDialog>
</template>

You can also use the #header, #body and #footer slots to customize the Modal's content.

Title

Use the title prop to set the title of the Modal's header.

<template>
  <PDialog title="Modal with title">
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDialog>
</template>

Description

Use the description prop to set the description of the Modal's header.

<template>
  <PDialog
    title="Modal with description"
    description="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
  >
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDialog>
</template>

Close

Use the close prop to customize or hide the close button (with false value) displayed in the Modal's header.

You can pass any property from the Button component to customize it.

<template>
  <PDialog
    title="Modal with close button"
    :close="{
      color: 'primary',
      variant: 'outline',
      class: 'rounded-full'
    }"
  >
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDialog>
</template>
The close button is not displayed if the #content slot is used as it's a part of the header.

Close Icon

Use the close-icon prop to customize the close button Icon. Defaults to i-lucide:x.

<template>
  <PDialog title="Modal with close button" close-icon="i-lucide:arrow-right">
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDialog>
</template>
You can customize this icon globally in your app.config.ts under pohon.icons.close key.
You can customize this icon globally in your vite.config.ts under pohon.icons.close key.

Overlay

Use the overlay prop to control whether the Modal has an overlay or not. Defaults to true.

<template>
  <PDialog title="Modal without overlay">
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDialog>
</template>

Use the modal prop to control whether the Modal blocks interaction with outside content. Defaults to true.

When modal is set to false, the overlay is automatically disabled and outside content becomes interactive.
<template>
  <PDialog title="Modal interactive">
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDialog>
</template>

Dismissible

Use the dismissible prop to control whether the Modal is dismissible when clicking outside of it or pressing escape. Defaults to true.

A close:prevent event will be emitted when the user tries to close it.
You can combine modal: false with dismissible: false to make the Modal's background interactive without closing it.
<template>
  <PDialog modal title="Modal non-dismissible">
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>
  </PDialog>
</template>

Fullscreen

Use the fullscreen prop to make the Modal fullscreen.

<template>
  <PDialog fullscreen title="Modal fullscreen">
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #body>
      <CorePlaceholder class="h-full" />
    </template>
  </PDialog>
</template>

Examples

Control open state

You can control the open state by using the default-open prop or the v-model:open directive.

<script setup lang="ts">
import { defineShortcuts } from '#imports';
import { ref } from 'vue';

const open = ref(false);

defineShortcuts({
  o: () => {
    open.value = !open.value;
  },
});
</script>

<template>
  <PDialog v-model:open="open">
    <PButton
      label="Open"
      color="neutral"
      variant="subtle"
    />

    <template #content>
      <CorePlaceholder class="m-4 h-48" />
    </template>
  </PDialog>
</template>
In this example, leveraging defineShortcuts, you can toggle the Modal by pressing O.
This allows you to move the trigger outside of the Modal or remove it entirely.

Programmatic usage

You can use the useOverlay composable to open a Modal programmatically.

Make sure to wrap your app with the App component which uses the OverlayProvider component.

First, create a modal component that will be opened programmatically:

ModalExample.vue
<script setup lang="ts">
defineProps<{
  count: number
}>()

const emit = defineEmits<{ close: [boolean] }>()
</script>

<template>
  <PDialog
    :close="{ onClick: () => emit('close', false) }"
    :title="`This modal was opened programmatically ${count} times`"
  >
    <template #footer>
      <div class="flex gap-2">
        <PButton color="neutral" label="Dismiss" @click="emit('close', false)" />
        <PButton label="Success" @click="emit('close', true)" />
      </div>
    </template>
  </PDialog>
</template>
We are emitting a close event when the modal is closed or dismissed here. You can emit any data through the close event, however, the event must be emitted in order to capture the return value.

Then, use it in your app:

<script setup lang="ts">
import { LazyModalExample } from '#components';
import { useOverlay, useToast } from '#imports';
import { ref } from 'vue';

const count = ref(0);

const toast = useToast();
const overlay = useOverlay();

const modal = overlay.create(LazyModalExample);

async function open() {
  const instance = modal.open({
    count: count.value,
  });

  const shouldIncrement = await instance.result;

  if (shouldIncrement) {
    count.value++;

    toast.add({
      title: `Success: ${shouldIncrement}`,
      color: 'success',
      id: 'modal-success',
    });

    // Update the count
    modal.patch({
      count: count.value,
    });
    return;
  }

  toast.add({
    title: `Dismissed: ${shouldIncrement}`,
    color: 'error',
    id: 'modal-dismiss',
  });
}
</script>

<template>
  <PButton
    label="Open"
    color="neutral"
    variant="subtle"
    @click="open"
  />
</template>
You can close the modal within the modal component by emitting emit('close').

Nested modals

You can nest modals within each other.

<script setup lang="ts">
import { ref } from 'vue';

const first = ref(false);
const second = ref(false);
</script>

<template>
  <PDialog
    v-model:open="first"
    title="First modal"
    :pohon="{ footer: 'justify-end' }"
  >
    <PButton
      color="neutral"
      variant="subtle"
      label="Open"
    />

    <template #footer>
      <PButton
        label="Close"
        color="neutral"
        variant="outline"
        @click="first = false"
      />

      <PDialog
        v-model:open="second"
        title="Second modal"
        :pohon="{ footer: 'justify-end' }"
      >
        <PButton
          label="Open second"
          color="neutral"
        />

        <template #footer>
          <PButton
            label="Close"
            color="neutral"
            variant="outline"
            @click="second = false"
          />
        </template>
      </PDialog>
    </template>
  </PDialog>
</template>

Use the #footer slot to add content after the Modal's body.

<script setup lang="ts">
import { ref } from 'vue';

const open = ref(false);
</script>

<template>
  <PDialog
    v-model:open="open"
    title="Modal with footer"
    description="This is useful when you want a form in a Modal."
    :pohon="{ footer: 'justify-end' }"
  >
    <PButton
      label="Open"
      color="neutral"
      variant="subtle"
    />

    <template #body>
      <CorePlaceholder class="h-48" />
    </template>

    <template #footer="{ close }">
      <PButton
        label="Cancel"
        color="neutral"
        variant="outline"
        @click="close"
      />
      <PButton
        label="Submit"
        color="neutral"
      />
    </template>
  </PDialog>
</template>

With command palette

You can use a CommandPalette component inside the Modal's content.

<script setup lang="ts">
import { useFetch } from '#app';
import { computed, ref } from 'vue';

const searchTerm = ref('');

const { data: users, status } = await useFetch('https://jsonplaceholder.typicode.com/users', {
  key: 'command-palette-users',
  params: { q: searchTerm },
  transform: (data: Array<{ id: number; name: string; email: string }>) => {
    return data?.map((user) => ({ id: user.id, label: user.name, suffix: user.email, avatar: { src: `https://i.pravatar.cc/120?img=${user.id}` } })) || [];
  },
  lazy: true,
});

const groups = computed(() => [{
  id: 'users',
  label: searchTerm.value ? `Users matching “${searchTerm.value}”...` : 'Users',
  items: users.value || [],
  ignoreFilter: true,
}]);
</script>

<template>
  <PDialog>
    <PButton
      label="Search users..."
      color="neutral"
      variant="subtle"
      icon="i-lucide:search"
    />

    <template #content>
      <PCommandPalette
        v-model:search-term="searchTerm"
        :loading="status === 'pending'"
        :groups="groups"
        placeholder="Search users..."
        class="h-80"
      />
    </template>
  </PDialog>
</template>

API

Props

Prop Default Type

Slots

Slot Type

Emits

Event Type

Theme

We use unocss-variants to customize the theme. Read more about it in the theming guide.

Below is the theme configuration skeleton for the PDialog. Since the component is provided unstyled by default, you will need to fill in these values to apply your own custom look and feel. If you prefer to use our pre-built, opinionated styling, you can instead use our UnoCSS preset, this docs is using it as well.

app.config.ts
export default defineAppConfig({
  pohon: {
    dialog: {
      slots: {
        overlay: '',
        content: '',
        header: '',
        wrapper: '',
        body: '',
        footer: '',
        title: '',
        description: '',
        close: ''
      },
      variants: {
        transition: {
          true: {
            overlay: '',
            content: ''
          }
        },
        fullscreen: {
          true: {
            content: ''
          },
          false: {
            content: ''
          }
        }
      }
    }
  }
};
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import pohon from 'pohon-ui/vite'

export default defineAppConfig({
  pohon: {
    dialog: {
      slots: {
        overlay: '',
        content: '',
        header: '',
        wrapper: '',
        body: '',
        footer: '',
        title: '',
        description: '',
        close: ''
      },
      variants: {
        transition: {
          true: {
            overlay: '',
            content: ''
          }
        },
        fullscreen: {
          true: {
            content: ''
          },
          false: {
            content: ''
          }
        }
      }
    }
  }
};

Akar

With Pohon UI, you can achieve similar component functionality with less code and effort, as it comes with built-in styles mechanism and behaviors that are optimized for common use cases. Since it's using unocss-variants it adds a runtime cost, but it can be worth it if you prioritize development speed and ease of use over fine-grained control.

If this is a deal breaker for you, you can always stick to using Akar and build your own custom components on top of it.

Changelog

No recent changes