Popover
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'radix-vue'
</script>
<template>
<PopoverRoot>
<PopoverTrigger
class="rounded-full w-[35px] h-[35px] inline-flex items-center justify-center text-grass11 bg-white shadow-[0_2px_10px] shadow-blackA7 hover:bg-green3 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.green7)] 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-grass11 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-grass11 shadow-[0_0_0_1px] shadow-green7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-green8 outline-none"
defaultValue="100%"
>
</fieldset>
<fieldset class="flex gap-5 items-center">
<label class="text-[13px] text-grass11 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-grass11 shadow-[0_0_0_1px] shadow-green7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-green8 outline-none"
defaultValue="300px"
>
</fieldset>
<fieldset class="flex gap-5 items-center">
<label class="text-[13px] text-grass11 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-grass11 shadow-[0_0_0_1px] shadow-green7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-green8 outline-none"
defaultValue="25px"
>
</fieldset>
<fieldset class="flex gap-5 items-center">
<label class="text-[13px] text-grass11 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-grass11 shadow-[0_0_0_1px] shadow-green7 h-[25px] focus:shadow-[0_0_0_2px] focus:shadow-green8 outline-none"
defaultValue="none"
>
</fieldset>
</div>
<PopoverClose
class="rounded-full h-[25px] w-[25px] inline-flex items-center justify-center text-grass11 absolute top-[5px] right-[5px] hover:bg-green4 focus:shadow-[0_0_0_2px] focus:shadow-green7 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 install radix-vueAnatomy
Import all parts and piece them together.
<script setup>
import { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'radix-vue'
</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 | booleanThe open state of the popover when it is initially rendered. Use when you do not need to control its open state. | |
open | boolean The controlled open state of the popover. Can be binded with | |
modal | false | boolean The modality of the popover. When set to |
| Emit | Type |
|---|---|
@update:open | (open: boolean) => void |
Trigger
The button that toggles the popover. By default, the PopoverContent will position itself against the trigger.
| Prop | Default | Type |
|---|---|---|
as | button | string | ComponentThe element or component this component should render as. Can be overwrite by |
asChild | false | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. |
| 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 |
|---|---|---|
as | div | string | ComponentThe element or component this component should render as. Can be overwrite by |
asChild | false | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. |
Portal
When used, portals the content part into the body.
| Prop | Default | Type |
|---|---|---|
to | body | string | HTMLElementVue native teleport component props. (to) |
Content
The component that pops out when the popover is open.
| Prop | Default | Type |
|---|---|---|
forceMount | booleanUsed to force mounting when more control is needed. Useful when controlling animation with Vue.js animation libraries. | |
side | "bottom" | enum The preferred side of the trigger to render against when open. Will be reversed when collisions occur and |
sideOffset | 0 | numberThe distance in pixels from the trigger. |
align | "center" | enumThe preferred alignment against the trigger. May change when collisions occur. |
alignOffset | 0 | number An offset in pixels from the |
avoidCollisions | true | boolean When |
collisionBoundary | [] | BoundaryThe element used as the collision boundary. By default this is the viewport, though you can provide additional element(s) to be included in this check. |
collisionPadding | 0 | number | Padding The distance in pixels from the boundary edges where collision detection should occur. Accepts a number (same for all sides), or a partial padding object, for example: |
arrowPadding | 0 | number The padding between the arrow and the edges of the content. If your content has |
sticky | "partial" | enum The sticky behavior on the align axis. |
hideWhenDetached | false | booleanWhether to hide the content when the trigger becomes fully occluded. |
as | div | string | ComponentThe element or component this component should render as. Can be overwrite by |
asChild | false | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. |
| Emit | Type |
|---|---|
@openAutoFocus | (event: Event) => void |
@closeAutoFocus | (event: Event) => void |
@escapeKeyDown | (event: KeyboardEvent) => void |
@pointerDownOutside | (event: PointerDownOutsideEvent) => void |
@interactOutside | (event: FocusEvent | MouseEvent | TouchEvent) => void |
| 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.
| Prop | Default | Type |
|---|---|---|
as | svg | string | ComponentThe element or component this component should render as. Can be overwrite by |
asChild | false | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. |
width | 10 | numberThe width of the arrow in pixels. |
height | 5 | numberThe height of the arrow in pixels. |
Close
The button that closes an open popover.
| Prop | Default | Type |
|---|---|---|
as | button | string | ComponentThe element or component this component should render as. Can be overwrite by |
asChild | false | booleanChange the default rendered element for the one passed as a child, merging their props and behavior. |
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 'radix-vue'
</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 'radix-vue'
</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 'radix-vue'
</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 { PopoverArrow, PopoverClose, PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'radix-vue'
</script>
<template>
<PopoverRoot>
<PopoverAnchor as-child>
<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 'radix-vue'<!-- PopoverContent.vue -->
<script setup lang="ts">
import { PopoverContent, type PopoverContentEmits, type PopoverContentProps, PopoverPortal, useForwardPropsEmits, } from 'radix-vue'
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>