An anchored surface for small settings, forms and contextual details.
pnpm dlx shadcn@latest add @latheui/popoverFirst time? Run npx latheui init once to add the @latheui registry. The CLI then writes the source into your project and installs what it needs. Or add it by hand:
Install the npm dependencies.
pnpm add motion radix-uiAdd the Lathe items this component imports.
lib/motion.tshooks/use-controllable-state.tsCopy the source into your project.
"use client"
import * as React from "react"
import { AnimatePresence, motion } from "motion/react"
import { Popover as Primitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { useControllableState } from "@/hooks/use-controllable-state"
import { fade, spring, useMotionSafe } from "@/lib/motion"
// Spread rather than written as an attribute: in Base UI projects the shadcn CLI
// rewrites a literal `asChild` into a `render` prop, and these are Radix parts.
const slot = { asChild: true } as const
const OpenContext = React.createContext(false)
export type PopoverProps = Omit<
React.ComponentProps<typeof Primitive.Root>,
"open" | "defaultOpen" | "onOpenChange"
> & {
open?: boolean
defaultOpen?: boolean
onOpenChange?: (open: boolean) => void
}
/** An anchored surface that opens from its trigger and stays until dismissed. */
export function Popover({ open: openProp, defaultOpen = false, onOpenChange, ...props }: PopoverProps) {
const [open, setOpen] = useControllableState({
value: openProp,
defaultValue: defaultOpen,
onChange: onOpenChange,
})
return (
// The content reads the open state, so it can stay mounted while it animates out.
<OpenContext.Provider value={open}>
<Primitive.Root open={open} onOpenChange={setOpen} {...props} />
</OpenContext.Provider>
)
}
export function PopoverTrigger(props: React.ComponentProps<typeof Primitive.Trigger>) {
return <Primitive.Trigger {...slot} {...props} />
}
export function PopoverContent({
className,
children,
sideOffset = 6,
...props
}: React.ComponentProps<typeof Primitive.Content>) {
const open = React.useContext(OpenContext)
const { reduced, safe } = useMotionSafe()
return (
<AnimatePresence>
{open && (
<Primitive.Portal forceMount>
<Primitive.Content {...slot} forceMount sideOffset={sideOffset} collisionPadding={8} {...props}>
<motion.div
data-slot="popover-content"
// Grows out of the trigger, and shrinks back a little less as it fades away.
initial={{ opacity: 0, scale: reduced ? 1 : 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: reduced ? 1 : 0.97, transition: fade }}
transition={safe(spring.snappy)}
className={cn(
"z-50 w-72 max-w-[calc(100vw-2rem)] origin-(--radix-popover-content-transform-origin) rounded-lg border border-border bg-popover p-4 text-sm text-popover-foreground shadow-lg outline-none",
className
)}
>
{children}
</motion.div>
</Primitive.Content>
</Primitive.Portal>
)}
</AnimatePresence>
)
}
export const PopoverClose = Primitive.CloseDepends on
| Prop | Type | Default | Description |
|---|---|---|---|
| open / defaultOpen | boolean | — | Controlled or initial visibility on the root. |
| onOpenChange | (open: boolean) => void | — | Visibility callback. |
| children | ReactNode | — | Compose Root, Trigger and Content. Trigger slots one child. |
| side / align / sideOffset | Radix placement props | — | Content placement and collision avoidance. |
Combine the control with your own layout. Semantic colors follow the active theme.
import { Popover, PopoverTrigger, PopoverContent, PopoverClose } from "@/components/ui/popover"
import { Button } from "@/components/ui/button"
export default function Demo(){return <Popover><PopoverTrigger><Button variant="outline">View settings</Button></PopoverTrigger><PopoverContent aria-label="Preview settings"><p className="font-medium">Preview settings</p><p className="mt-1 text-muted-foreground">Changes apply to this workspace only.</p><PopoverClose {...{asChild:true}}><Button className="mt-4" size="sm">Done</Button></PopoverClose></PopoverContent></Popover>}