Primitives
Popover
Anchored floating panel for inline settings and contextual content.
Composition
Resizing content
PopoverViewport animates content height with the shared motion tokens and preserves automatic sizing before hydration.
- <Popover>
- <PopoverTrigger />
- <PopoverContent>
- <PopoverViewport>
- changing panel content
- <PopoverViewport>
Anchored inspector
- <Popover>
- <PopoverTrigger />
- <PopoverContent>
- <PopoverHeader>
- <PopoverTitle />
- <PopoverDescription />
- panel content
- <PopoverClose />
- <PopoverHeader>
Separate positioning anchor
- <Popover>
- <PopoverAnchor>
- anchor content
- <PopoverTrigger />
- <PopoverContent>
- panel content
- <PopoverAnchor>
Installation
First install and activate one skin. Core deliberately contains no visual token defaults.
The Control UI source installs this primitive from src/registry/sources/control-ui/ui/popover.tsx.
npx shadcn@latest add https://control-ui.dev/r/popover.jsonRaw code
Primary installed source
"use client";import { Popover as PopoverPrimitive } from "@base-ui/react/popover";import { type ComponentProps, type CSSProperties, useImperativeHandle, useLayoutEffect, useRef } from "react";import type { PopupKnobStyle } from "@/components/control-ui/knob-contracts/popup-knobs";import { cn } from "@/components/control-ui/lib/cn";import { controlEffectsAttribute } from "@/components/control-ui/skin";import { useSkin } from "@/components/control-ui/skin-provider";export const popoverContentPaddings = ["default", "none"] as const;export type PopoverContentPadding = (typeof popoverContentPaddings)[number];type PopoverPopupProps = Omit<ComponentProps<typeof PopoverPrimitive.Popup>, "style"> & { side?: ComponentProps<typeof PopoverPrimitive.Positioner>["side"]; align?: ComponentProps<typeof PopoverPrimitive.Positioner>["align"]; sideOffset?: number; collisionPadding?: ComponentProps<typeof PopoverPrimitive.Positioner>["collisionPadding"]; padding?: PopoverContentPadding; style?: CSSProperties & PopupKnobStyle;};export function Popover(props: ComponentProps<typeof PopoverPrimitive.Root>) { return <PopoverPrimitive.Root {...props} />;}export function PopoverTrigger({ className, ...props}: ComponentProps<typeof PopoverPrimitive.Trigger> & { style?: CSSProperties & PopupKnobStyle }) { return ( <PopoverPrimitive.Trigger data-control-ui="popover" data-popup-kind="popover" data-slot="trigger" className={className} {...props} /> );}export function PopoverAnchor(props: ComponentProps<typeof PopoverPrimitive.Trigger>) { return <PopoverPrimitive.Trigger {...props} />;}export function PopoverContent({ className, children, side = "bottom", align = "center", sideOffset = 6, collisionPadding, padding = "default", ...props}: PopoverPopupProps) { const skin = useSkin(); return ( <PopoverPrimitive.Portal> <PopoverPrimitive.Positioner data-skin={skin.id} data-effects={controlEffectsAttribute(skin.effects)} side={side} align={align} sideOffset={sideOffset} collisionPadding={collisionPadding} className="z-(--z-popup)" > <PopoverPrimitive.Popup data-control-ui="popover" data-popup-kind="popover" data-slot="content" data-surface="floating" data-control-family="popup" data-popup-part="surface" data-padding={padding} className={cn("w-72", className)} {...props} > {children} </PopoverPrimitive.Popup> </PopoverPrimitive.Positioner> </PopoverPrimitive.Portal> );}export function PopoverViewport({ children, ref, ...props }: ComponentProps<"div"> & { style?: CSSProperties & PopupKnobStyle }) { const viewportRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null); useImperativeHandle(ref, () => { if (!viewportRef.current) throw new Error("PopoverViewport is not mounted."); return viewportRef.current; }, []); useLayoutEffect(() => { const viewport = viewportRef.current; const content = contentRef.current; if (!viewport || !content) return; const updateHeight = () => viewport.style.setProperty("--_popover-viewport-height", `${content.offsetHeight}px`); updateHeight(); const observer = new ResizeObserver(updateHeight); observer.observe(content); return () => observer.disconnect(); }, []); return ( <div data-control-ui="popover" data-control-family="popup" data-popup-kind="popover" data-slot="viewport" {...props} ref={viewportRef}> <div data-control-ui="popover" data-control-family="popup" data-popup-kind="popover" data-slot="viewport-content" ref={contentRef}> {children} </div> </div> );}export function PopoverHeader({ className, ...props }: ComponentProps<"div"> & { style?: CSSProperties & PopupKnobStyle }) { return ( <div data-control-ui="popover" data-control-family="popup" data-popup-kind="popover" data-slot="header" className={cn("grid", className)} {...props} /> );}export function PopoverTitle({ className, ...props}: ComponentProps<typeof PopoverPrimitive.Title> & { style?: CSSProperties & PopupKnobStyle }) { return ( <PopoverPrimitive.Title data-control-ui="popover" data-control-family="popup" data-popup-kind="popover" data-slot="title" className={className} {...props} /> );}export function PopoverDescription({ className, ...props}: ComponentProps<typeof PopoverPrimitive.Description> & { style?: CSSProperties & PopupKnobStyle }) { return ( <PopoverPrimitive.Description data-control-ui="popover" data-control-family="popup" data-popup-kind="popover" data-slot="description" className={className} {...props} /> );}export function PopoverClose({ className, ...props}: ComponentProps<typeof PopoverPrimitive.Close> & { style?: CSSProperties & PopupKnobStyle }) { return ( <PopoverPrimitive.Close data-control-ui="popover" data-control-family="popup" data-popup-kind="popover" data-slot="close" className={className} {...props} /> );}Knobs
Typed custom properties the recipe paints with. Set one on the root — style, a utility class, or a skin — and every slot inherits it.
--cui-popup-* · 44 knobsHow the cascade resolves--cui-popup-title-foreground<color>var(--cui-popup-foreground)--cui-popup-title-font-size<length-percentage>var(--text-heading-3)--cui-popup-title-font-weight<number>var(--font-weight-semibold)--cui-popup-title-line-height<number>var(--text-heading-3--line-height)--cui-popup-title-letter-spacing<length>-0.025em--cui-popup-description-foreground<color>var(--muted-foreground)--cui-popup-close-inset<length-percentage>calc(var(--spacing) * 3)--cui-popup-item-inset<length-percentage>calc(var(--spacing) * 8)--cui-popup-label-padding-block<length-percentage>var(--cui-popup-item-padding-block)--cui-popup-label-padding-inline<length-percentage>var(--cui-popup-item-padding-inline)--cui-popup-label-font-size<length-percentage>var(--text-micro)--cui-popup-label-font-weight<number>var(--font-weight-medium)--cui-popup-label-letter-spacing<length>0.08em--cui-popup-label-text-transformnone | uppercase | lowercase | capitalizeuppercase--cui-popup-separator-inset<length-percentage>calc(var(--popover-padding) * -1)--cui-popup-shortcut-font-size<length-percentage>var(--text-body)--cui-popup-shortcut-letter-spacing<length>0.02em--cui-popup-status-success-foreground<color>var(--success-text)--cui-popup-status-warning-foreground<color>var(--warning-text)--cui-popup-status-info-foreground<color>var(--info-text)--cui-popup-status-error-foreground<color>var(--destructive)--cui-popup-radius<length-percentage>var(--radius-popover)--cui-popup-background<color>var(--popover)--cui-popup-foreground<color>var(--popover-foreground)--cui-popup-border-color<color>var(--control-rim)--cui-popup-border-width<length>var(--control-rim-width)--cui-popup-shadow*var(--shadow-pop)--cui-popup-backdrop-filter*blur(var(--backdrop-blur-popover))--cui-popup-backdrop-background<color>oklch(from var(--foreground) l c h / var(--overlay-opacity))--cui-popup-backdrop-blur<length>var(--backdrop-blur-overlay)--cui-popup-item-radius<length-percentage>var(--radius-popup-item-fit)--cui-popup-item-font-size<length-percentage>var(--text-body)--cui-popup-item-icon-size<length-percentage>1rem--cui-popup-item-padding-block<length-percentage>calc(var(--spacing) * 1)--cui-popup-item-padding-inline<length-percentage>calc(var(--padding-x) * 0.5)--cui-popup-item-min-height<length-percentage>max(var(--target-min), var(--control-h-xs))--cui-popup-item-highlight-shadow*inset 0 0 0 1px oklch(from var(--foreground) l c h / 0.35)--cui-popup-item-foreground<color>var(--foreground)--cui-popup-item-highlight-background<color>var(--hover-fill)--cui-popup-item-highlight-foreground<color>var(--cui-popup-item-foreground)--cui-popup-item-highlight-muted-foreground<color>var(--muted-foreground)--cui-popup-item-disabled-opacity<number>var(--disabled-opacity)--cui-popup-separator-color<color>var(--border)--cui-popup-shortcut-foreground<color>var(--muted-foreground)