Popover

A non-modal dialog that floats around a trigger element.

Usage

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

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

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

    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template>
  </PPopover>
</template>

Mode

Use the mode prop to change the mode of the Popover. Defaults to click.

In hover mode, set the enable-touch prop to let users toggle the Popover by tapping the trigger on touch devices, or use the click mode for triggers meant to be tapped.
<template>
  <PPopover mode="hover" enable-touch>
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template>
  </PPopover>
</template>
When using the hover mode, the Akar HoverCard component is used instead of the Popover.

Delay

When using the hover mode, you can use the open-delay and close-delay props to control the delay before the Popover is opened or closed.

<template>
  <PPopover mode="hover" :open-delay="500" :close-delay="300">
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template>
  </PPopover>
</template>

Content

Use the content prop to control how the Popover content is rendered, like its align or side for example.

<template>
  <PPopover
    :content="{
      align: 'center',
      side: 'bottom',
      sideOffset: 8
    }"
  >
    <PButton label="Open" color="neutral" variant="subtle" />

    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template>
  </PPopover>
</template>

Arrow

Use the arrow prop to display an arrow on the Popover.

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

    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template>
  </PPopover>
</template>

Use the modal prop to control whether the Popover blocks interaction with outside content. Defaults to false.

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

    <template #content>
      <Placeholder class="size-48 m-4 inline-flex" />
    </template>
  </PPopover>
</template>

Dismissible

Use the dismissible prop to control whether the Popover 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.
<template>
  <PPopover
    :dismissible="false"
    :ui="{ content: 'p-4' }"
  >
    <PButton
      label="Open"
      color="neutral"
      variant="subtle"
    />

    <template #content="{ close }">
      <div class="mb-4 flex gap-4 items-center">
        <h2 class="color-text-highlighted font-semibold">
          Popover non-dismissible
        </h2>

        <PButton
          color="neutral"
          variant="ghost"
          icon="i-lucide-x"
          @click="close"
        />
      </div>

      <Placeholder class="size-full min-h-48" />
    </template>
  </PPopover>
</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">
const open = ref(false);

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

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

    <template #content>
      <Placeholder class="m-4 inline-flex size-48" />
    </template>
  </PPopover>
</template>
<script setup lang="ts">
import { ref } from 'vue'

const open = ref(false);

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

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

    <template #content>
      <Placeholder class="m-4 inline-flex size-48" />
    </template>
  </PPopover>
</template>
In this example, leveraging defineShortcuts, you can toggle the Popover by pressing O.

With command palette

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

<script setup lang="ts">
import type { CommandPaletteItem } from 'pohon-ui';

const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error',
    },
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success',
    },
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info',
    },
  },
] satisfies Array<CommandPaletteItem>);

const label = ref([]);
</script>

<template>
  <PPopover :content="{ side: 'right', align: 'start' }">
    <PButton
      icon="i-lucide-tag"
      label="Select labels"
      color="neutral"
      variant="subtle"
    />

    <template #content>
      <PCommandPalette
        v-model="label"
        multiple
        placeholder="Search labels..."
        :groups="[{ id: 'labels', items }]"
        :ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }"
      />
    </template>
  </PPopover>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { CommandPaletteItem } from 'pohon-ui';

const items = ref([
  {
    label: 'bug',
    value: 'bug',
    chip: {
      color: 'error',
    },
  },
  {
    label: 'feature',
    value: 'feature',
    chip: {
      color: 'success',
    },
  },
  {
    label: 'enhancement',
    value: 'enhancement',
    chip: {
      color: 'info',
    },
  },
] satisfies Array<CommandPaletteItem>);

const label = ref([]);
</script>

<template>
  <PPopover :content="{ side: 'right', align: 'start' }">
    <PButton
      icon="i-lucide-tag"
      label="Select labels"
      color="neutral"
      variant="subtle"
    />

    <template #content>
      <PCommandPalette
        v-model="label"
        multiple
        placeholder="Search labels..."
        :groups="[{ id: 'labels', items }]"
        :ui="{ input: '[&>input]:h-8 [&>input]:text-sm' }"
      />
    </template>
  </PPopover>
</template>

With following cursor

You can make the Popover follow the cursor when hovering over an element using the reference prop:

<script setup lang="ts">
const open = ref(false);
const anchor = ref({ x: 0, y: 0 });

const reference = computed(() => ({
  getBoundingClientRect: () =>
    ({
      width: 0,
      height: 0,
      left: anchor.value.x,
      right: anchor.value.x,
      top: anchor.value.y,
      bottom: anchor.value.y,
      ...anchor.value,
    } as DOMRect),
}));
</script>

<template>
  <PPopover
    :open="open"
    :reference="reference"
    :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
  >
    <div
      class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center"
      @pointerenter="open = true"
      @pointerleave="open = false"
      @pointermove="(ev: PointerEvent) => {
        anchor.x = ev.clientX
        anchor.y = ev.clientY
      }"
    >
      Hover me
    </div>

    <template #content>
      <div class="p-4">
        {{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
      </div>
    </template>
  </PPopover>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'

const open = ref(false);
const anchor = ref({ x: 0, y: 0 });

const reference = computed(() => ({
  getBoundingClientRect: () =>
    ({
      width: 0,
      height: 0,
      left: anchor.value.x,
      right: anchor.value.x,
      top: anchor.value.y,
      bottom: anchor.value.y,
      ...anchor.value,
    } as DOMRect),
}));
</script>

<template>
  <PPopover
    :open="open"
    :reference="reference"
    :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
  >
    <div
      class="text-sm border border-border-accented rounded-md border-dashed flex w-72 aspect-video items-center justify-center"
      @pointerenter="open = true"
      @pointerleave="open = false"
      @pointermove="(ev: PointerEvent) => {
        anchor.x = ev.clientX
        anchor.y = ev.clientY
      }"
    >
      Hover me
    </div>

    <template #content>
      <div class="p-4">
        {{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
      </div>
    </template>
  </PPopover>
</template>

With anchor slot

You can use the #anchor slot to position the Popover against a custom element.

This slot only works when mode is click.
<script lang="ts" setup>
const open = ref(false);
</script>

<template>
  <PPopover
    v-model:open="open"
    :dismissible="false"
    :ui="{ content: 'w-(--akar-popover-trigger-width) p-4' }"
  >
    <template #anchor>
      <PInput
        placeholder="Focus to open"
        @focus="open = true"
        @blur="open = false"
      />
    </template>

    <template #content>
      <Placeholder class="w-full aspect-square" />
    </template>
  </PPopover>
</template>

API

Props

Prop Default Type

Slots

Slot Type
The close function is only available when mode is set to click because Akar exposes this for Popover but not for HoverCard.

Emits

Event Type

Theme

app.config.ts
export default defineAppConfig({
  ui: {
    popover: {
      slots: {
        content: 'rounded-md bg-background pointer-events-auto ring ring-border shadow-lg origin-$akar-popover-content-transform-origin focus:outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
        arrow: 'fill-background stroke-border'
      }
    }
  }
})
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import ui from 'pohon-ui/vite'

export default defineConfig({
  plugins: [
    vue(),
    ui({
      ui: {
        popover: {
          slots: {
            content: 'rounded-md bg-background pointer-events-auto ring ring-border shadow-lg origin-$akar-popover-content-transform-origin focus:outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
            arrow: 'fill-background stroke-border'
          }
        }
      }
    })
  ]
})

Changelog

No recent changes