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.
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>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>Modal
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.
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>
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.
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 |
|---|
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
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'
}
}
}
})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'
}
}
}
})
]
})