Popover
A slip of paper held up next to its trigger, pencilled round and lifted off its shadow.
Built on Base UI Popoverimport { Button } from "@/components/ui/button";
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@/components/ui/popover";
export default function PopoverDemo() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" seed="popover-open" />}>Page size</PopoverTrigger>
<PopoverContent seed="popover" className="w-80">
<PopoverHeader>
<PopoverTitle>Page size</PopoverTitle>
<PopoverDescription>In millimetres, before trimming.</PopoverDescription>
</PopoverHeader>
<Field orientation="horizontal">
<FieldLabel htmlFor="popover-width" className="w-20">
Width
</FieldLabel>
<Input id="popover-width" defaultValue="148" seed="popover-width" />
</Field>
<Field orientation="horizontal">
<FieldLabel htmlFor="popover-height" className="w-20">
Height
</FieldLabel>
<Input id="popover-height" defaultValue="210" seed="popover-height" />
</Field>
</PopoverContent>
</Popover>
);
}Installation
pnpm dlx shadcn@latest add @ballpoint/ popoverOr copy the source
"use client";
import type { ComponentProps } from "react";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { cn } from "@/lib/utils";
import { usePen, type Pen } from "@/hooks/use-ink-box";
import { InkPanel } from "@/lib/ink-panel";
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
}
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
/** A slip of paper held up next to its trigger, pencilled round and lifted off its shadow. */
function PopoverContent({
className,
children,
align = "center",
alignOffset = 0,
side = "bottom",
sideOffset = 8,
seed,
roughness,
passes,
radius,
corners,
shadow,
draw,
weight,
speed,
...props
}: PopoverPrimitive.Popup.Props &
Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset"> &
Omit<Pen, "fill"> & { seed?: string | number }) {
const pen = usePen({ roughness, passes, radius, corners, shadow, draw, weight, speed });
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner align={align} alignOffset={alignOffset} side={side} sideOffset={sideOffset} className="isolate z-50">
<PopoverPrimitive.Popup
data-slot="popover-content"
className={cn(
"ink-paper relative isolate flex w-72 origin-(--transform-origin) flex-col gap-3 p-4 text-base text-popover-foreground outline-none",
"transition-[opacity,scale] duration-(--dur-hover) ease-out data-starting-style:opacity-0 data-ending-style:opacity-0 data-ending-style:ease-in",
"motion-safe:data-starting-style:scale-[0.97] motion-safe:data-ending-style:scale-[0.98]",
className,
)}
{...props}
>
<InkPanel pen={pen} seed={seed} estimate={[288, 140]} offset={5} className="text-ink-3" />
{children}
</PopoverPrimitive.Popup>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
);
}
function PopoverHeader({ className, ...props }: ComponentProps<"div">) {
return <div data-slot="popover-header" className={cn("flex flex-col gap-1", className)} {...props} />;
}
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return <PopoverPrimitive.Title data-slot="popover-title" className={cn("text-lg leading-snug font-bold", className)} {...props} />;
}
function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
return <PopoverPrimitive.Description data-slot="popover-description" className={cn("text-base text-ink-3", className)} {...props} />;
}
export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };Usage
import { Button } from "@/components/ui/button"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Open</PopoverTrigger>
<PopoverContent>Place content here.</PopoverContent>
</Popover>API reference
- side
"top" | "right" | "bottom" | "left" | "inline-start" | "inline-end"default"bottom" - On PopoverContent: which side of the trigger it opens on.
- align
"start" | "center" | "end"default"center" - How it lines up with the trigger along that side.
- sideOffset / alignOffset
numberdefault8 / 0 - Gap from the trigger, and nudge along it, in px.
- seed
string | number - Pins the drawing.
Every other prop goes to Base UI's Popover.
Pen settings
The drawing also takes draw, roughness, passes, radius, corners, shadow, weight and speed, as props or from the nearest InkProvider. What each one does.