Popover

Displays rich content in a portal, triggered by a button.

"use client"

import * as React from "react"
import { cn } from "cn"
import { Popover as PopoverPrimitive } from "@animate-ui/react/primitives/radix-ui"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

function Popover({
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
  return <PopoverPrimitive.Root data-slot="popover" {...props} />
}

function PopoverTrigger({
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
  return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
}

function PopoverContent({
  className,
  align = "center",
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
  return (
    <PopoverPrimitive.Portal>
      <PopoverPrimitive.Content
        data-slot="popover-content"
        align={align}
        sideOffset={sideOffset}
        className={cn(
          "bg-popover text-popover-foreground ring-foreground/10 flex flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 z-50 w-72 origin-(--radix-popover-content-transform-origin) outline-hidden",
          className
        )}
        {...props}
      />
    </PopoverPrimitive.Portal>
  )
}

function PopoverAnchor({
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}

function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="popover-header"
      className={cn("flex flex-col gap-0.5 text-sm", className)}
      {...props}
    />
  )
}

function PopoverTitle({ className, ...props }: React.ComponentProps<"h2">) {
  return (
    <div
      data-slot="popover-title"
      className={cn("font-medium", className)}
      {...props}
    />
  )
}

function PopoverDescription({
  className,
  ...props
}: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="popover-description"
      className={cn("text-muted-foreground", className)}
      {...props}
    />
  )
}

export {
  Popover,
  PopoverAnchor,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
}

export default function PopoverDemo({
  side = "bottom",
  sideOffset = 4,
  align = "center",
  alignOffset = 0,
}: {
  side?: "top" | "right" | "bottom" | "left"
  sideOffset?: number
  align?: "start" | "center" | "end"
  alignOffset?: number
}) {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Open popover</Button>
      </PopoverTrigger>
      <PopoverContent
        className="w-80"
        side={side}
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
      >
        <div className="grid gap-4">
          <div className="space-y-2">
            <h4 className="leading-none font-medium">Dimensions</h4>
            <p className="text-sm text-muted-foreground">
              Set the dimensions for the layer.
            </p>
          </div>
          <div className="grid gap-2">
            <div className="grid grid-cols-3 items-center gap-4">
              <Label htmlFor="width">Width</Label>
              <Input
                id="width"
                defaultValue="100%"
                className="col-span-2 h-8"
              />
            </div>
            <div className="grid grid-cols-3 items-center gap-4">
              <Label htmlFor="maxWidth">Max. width</Label>
              <Input
                id="maxWidth"
                defaultValue="300px"
                className="col-span-2 h-8"
              />
            </div>
            <div className="grid grid-cols-3 items-center gap-4">
              <Label htmlFor="height">Height</Label>
              <Input
                id="height"
                defaultValue="25px"
                className="col-span-2 h-8"
              />
            </div>
            <div className="grid grid-cols-3 items-center gap-4">
              <Label htmlFor="maxHeight">Max. height</Label>
              <Input
                id="maxHeight"
                defaultValue="none"
                className="col-span-2 h-8"
              />
            </div>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add popover
pnpm dlx shadcn@latest add popover
yarn dlx shadcn@latest add popover
bun x shadcn@latest add popover

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate popover
pnpm dlx @animate-ui/cli@latest animate popover
yarn dlx @animate-ui/cli@latest animate popover
bun x @animate-ui/cli@latest animate popover

Usage

Import the primitive from Animate UI instead of Radix UI. Everything else stays the same.

import { Popover } from '@animate-ui/react/primitives/radix-ui';

export function SharePopover() {
  return (
    <Popover.Root>
      <Popover.Trigger>Share</Popover.Trigger>
      <Popover.Portal>
        <Popover.Content sideOffset={4}>
          Anyone with the link can view this document.
          <Popover.Close aria-label="Close">×</Popover.Close>
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  );
}

Coming from the @radix-ui/react-popover package? Use the subpath: import * as PopoverPrimitive from '@animate-ui/react/primitives/radix-ui/popover'.

Animation

The motion of Animate UI v1's Popover, value for value:

  • Content grows from 0.8 and fades in, from the trigger (--radix-popover-content-transform-origin, set inline so it works without styles), on a spring (stiffness 400, damping 29) that overshoots a little before it settles, by ~440ms.
  • It closes the same way: it shrinks back to 0.8 on the same spring while it fades out, then unmounts.
  • Opening from the keyboard animates the same way. Focus goes back to the trigger once the exit is over.
  • With forceMount (on the Content or the Portal), the closed content stays in the DOM: it plays its exit, then is hidden and inert.
  • Reopening while it closes picks up from where it is, on the same spring.
  • Open from the start (defaultOpen), it plays the same entrance on page load, like v1.
  • Under reduced motion, it only fades (no scale).

API Reference

Every part takes exactly the props of Radix UI's Popover. Animate UI adds the animation props below.

Root, Trigger, Anchor, Portal, Close, Arrow

Radix UI props only. They are not animated.

Content

Radix UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 400, damping: 29 }
exitTransition?
Transitiontransition

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page