Popover
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from '@oku-ui/primitives'
</script>
<template>
<PopoverRoot>
<PopoverTrigger
class="rounded-full w-[35px] h-[35px] inline-flex items-center justify-center text-mauve12 bg-white shadow-[0_2px_10px] shadow-blackA7 hover:bg-indigo3 focus:shadow-[0_0_0_2px] focus:shadow-black cursor-default outline-none"
aria-label="Update dimensions"
>
<Icon icon="radix-icons:mixer-horizontal" />
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
side="bottom"
:side-offset="5"
class="rounded p-5 w-[260px] bg-white shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2)] focus:shadow-[0_10px_38px_-10px_hsla(206,22%,7%,.35),0_10px_20px_-15px_hsla(206,22%,7%,.2),0_0_0_2px_theme(colors.indigo7)] will-change-[transform,opacity] data-[state=open]:data-[side=top]:animate-slideDownAndFade data-[state=open]:data-[side=right]:animate-slideLeftAndFade data-[state=open]:data-[side=bottom]:animate-slideUpAndFade data-[state=open]:data-[side=left]:animate-slideRightAndFade"
>
<div class="flex flex-col gap-2.5">
<p class="text-mauve12 text-[15px] leading-[19px] font-semibold mb-2.5">
Dimensions
</p>
<fieldset class="flex gap-5 items-center">
<label
class="text-[13px] text-mauve12 w-[75px]"
for="width"
> Width </label>
<input
id="width"
class="w-full inline-flex items-center justify-center flex-1 rounded px-2.5 text-[13px] leading-none text-mauve12 shadow-[0_0_0_1px] shadow-indigo7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-indigo8 outline-none"
defaultValue="100%"
>
</fieldset>
<fieldset class="flex gap-5 items-center">
<label
class="text-[13px] text-mauve12 w-[75px]"
for="maxWidth"
> Max. width </label>
<input
id="maxWidth"
class="w-full inline-flex items-center justify-center flex-1 rounded px-2.5 text-[13px] leading-none text-mauve12 shadow-[0_0_0_1px] shadow-indigo7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-indigo8 outline-none"
defaultValue="300px"
>
</fieldset>
<fieldset class="flex gap-5 items-center">
<label
class="text-[13px] text-mauve12 w-[75px]"
for="height"
> Height </label>
<input
id="height"
class="w-full inline-flex items-center justify-center flex-1 rounded px-2.5 text-[13px] leading-none text-mauve12 shadow-[0_0_0_1px] shadow-indigo7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-indigo8 outline-none"
defaultValue="25px"
>
</fieldset>
<fieldset class="flex gap-5 items-center">
<label
class="text-[13px] text-mauve12 w-[75px]"
for="maxHeight"
> Max. height </label>
<input
id="maxHeight"
class="w-full inline-flex items-center justify-center flex-1 rounded px-2.5 text-[13px] leading-none text-mauve12 shadow-[0_0_0_1px] shadow-indigo7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-indigo8 outline-none"
defaultValue="none"
>
</fieldset>
</div>
<PopoverClose
class="rounded-full h-[25px] w-[25px] inline-flex items-center justify-center text-mauve12 absolute top-[5px] right-[5px] hover:bg-indigo4 focus:shadow-[0_0_0_2px] focus:shadow-indigo7 outline-none cursor-default"
aria-label="Close"
>
<Icon icon="radix-icons:cross-2" />
</PopoverClose>
<PopoverArrow class="fill-white" />
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
Features
- Can be controlled or uncontrolled.
- Customize side, alignment, offsets, collision handling.
- Optionally render a pointing arrow.
- Focus is fully managed and customizable.
- Supports modal and non-modal modes.
- Dismissing and layering behavior is highly customizable.
Installation
Install the component from your command line.
$ npm add @oku-ui/primitives
Anatomy
Import all parts and piece them together.
<script setup>
import { PopoverAnchor, PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from '@oku-ui/primitives'
</script>
<template>
<PopoverRoot>
<PopoverTrigger />
<PopoverAnchor />
<PopoverPortal>
<PopoverContent>
<PopoverClose />
<PopoverArrow />
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
API Reference
Root
Contains all the parts of a popover.
Prop | Default | Type |
---|---|---|
defaultOpen | boolean | |
modal | boolean | |
open | boolean |
Emit | Payload |
---|---|
update:open | [value: boolean] |
Trigger
The button that toggles the popover. By default, the PopoverContent
will position itself against the trigger.
Prop | Default | Type |
---|---|---|
as | 'div' | object | AsTag |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
Anchor
An optional element to position the PopoverContent
against. If this part is not used, the content will position alongside the PopoverTrigger
.
Prop | Default | Type |
---|---|---|
virtualRef | MutableRefObject<Measurable> |
Portal
When used, portals the content part into the body
.
Content
The component that pops out when the popover is open.
Prop | Default | Type |
---|---|---|
forceMount | boolean Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
Data Attribute | Value |
---|---|
[data-state] | "open" | "closed" |
[data-side] | "left" | "right" | "bottom" | "top" |
[data-align] | "start" | "end" | "center" |
CSS Variable | Description |
---|---|
--radix-popover-content-transform-origin | The transform-origin computed from the content and arrow positions/offsets |
--radix-popover-content-available-width | The remaining width between the trigger and the boundary edge |
--radix-popover-content-available-height | The remaining height between the trigger and the boundary edge |
--radix-popover-trigger-width | The width of the trigger |
--radix-popover-trigger-height | The height of the trigger |
Arrow
An optional arrow element to render alongside the popover. This can be used to help visually link the anchor with the PopoverContent
. Must be rendered inside PopoverContent
.
Close
The button that closes an open popover.
Prop | Default | Type |
---|---|---|
as | 'div' | object | AsTag |
Examples
Constrain the content size
You may want to constrain the width of the content so that it matches the trigger width. You may also want to constrain its height to not exceed the viewport.
We expose several CSS custom properties such as --radix-popover-trigger-width
and --radix-popover-content-available-height
to support this. Use them to constrain the content dimensions.
// index.vue
<script setup>
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from '@oku-ui/primitives'
</script>
<template>
<PopoverRoot>
<PopoverTrigger>…</PopoverTrigger>
<PopoverPortal>
<PopoverContent
class="PopoverContent"
:side-offset="5"
>
…
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
/* styles.css */
.PopoverContent {
width: var(--radix-popover-trigger-width);
max-height: var(--radix-popover-content-available-height);
}
Origin-aware animations
We expose a CSS custom property --radix-popover-content-transform-origin
. Use it to animate the content from its computed origin based on side
, sideOffset
, align
, alignOffset
and any collisions.
// index.vue
<script setup>
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from '@oku-ui/primitives'
</script>
<template>
<PopoverRoot>
<PopoverTrigger>…</PopoverTrigger>
<PopoverPortal>
<PopoverContent class="PopoverContent">
…
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
/* styles.css */
.PopoverContent {
transform-origin: var(--radix-popover-content-transform-origin);
animation: scaleIn 0.5s ease-out;
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0);
}
to {
opacity: 1;
transform: scale(1);
}
}
Collision-aware animations
We expose data-side
and data-align
attributes. Their values will change at runtime to reflect collisions. Use them to create collision and direction-aware animations.
// index.vue
<script setup>
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from '@oku-ui/primitives'
</script>
<template>
<PopoverRoot>
<PopoverTrigger>…</PopoverTrigger>
<PopoverPortal>
<PopoverContent class="PopoverContent">
…
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
/* styles.css */
.PopoverContent {
animation-duration: 0.6s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
}
.PopoverContent[data-side="top"] {
animation-name: slideUp;
}
.PopoverContent[data-side="bottom"] {
animation-name: slideDown;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
With custom anchor
You can anchor the content to another element if you do not want to use the trigger as the anchor.
// index.vue
<script setup>
import { PopoverAnchor, PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from '@oku-ui/primitives'
</script>
<template>
<PopoverRoot>
<PopoverAnchor as="template">
<div class="Row">
Row as anchor <PopoverTrigger>Trigger</PopoverTrigger>
</div>
</PopoverAnchor>
<PopoverPortal>
<PopoverContent>…</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
/* styles.css */
.Row {
background-color: gainsboro;
padding: 20px;
}
Accessibility
Adheres to the Dialog WAI-ARIA design pattern.
Keyboard Interactions
Key | Description |
---|---|
Space | Opens/closes the popover. |
Enter | Opens/closes the popover. |
Tab | Moves focus to the next focusable element |
Shift + Tab | Moves focus to the previous focusable element |
Esc | Closes the popover and moves focus to PopoverTrigger . |
Custom APIs
Create your own API by abstracting the primitive parts into your own component.
Abstract the arrow and set default configuration
This example abstracts the PopoverArrow
part and sets a default sideOffset
configuration.
Usage
<script setup lang="ts">
import { Popover, PopoverContent, PopoverTrigger } from './your-popover'
</script>
<template>
<Popover>
<PopoverTrigger>Popover trigger</PopoverTrigger>
<PopoverContent>Popover content</PopoverContent>
</Popover>
</template>
Implementation
// your-popover.ts
export { default as PopoverContent } from 'PopoverContent.vue'
export { PopoverRoot as Popover, PopoverTrigger } from '@oku-ui/primitives'
<!-- PopoverContent.vue -->
<script setup lang="ts">
import { PopoverContent, type PopoverContentEmits, type PopoverContentProps, PopoverPortal, useForwardPropsEmits, } from '@oku-ui/primitives'
const props = defineProps<PopoverContentProps>()
const emits = defineEmits<PopoverContentEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<PopoverPortal>
<PopoverContent v-bind="{ ...forwarded, ...$attrs }">
<slot />
</PopoverContent>
</PopoverPortal>
</template>