A focus-managed compact surface for controls or details, with collision handling, mobile-safe width, and optional close action.
Project access
Team only
"use client";
import * as React from "react";
import { Link2, LockKeyhole } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/components/ballmac/popover";
export default function PopoverDemo() {
const [access, setAccess] = React.useState("Team only");
return (
<div className="w-full max-w-sm rounded-xl border bg-card p-5 shadow-sm">
<div className="flex items-center gap-3">
<span className="flex size-10 items-center justify-center rounded-lg bg-muted">
<LockKeyhole aria-hidden="true" className="size-5" />
</span>
<div className="min-w-0 flex-1">
<p className="text-sm font-semibold">Project access</p>
<p className="text-xs text-muted-foreground">{access}</p>
</div>
<Popover defaultOpen>
<PopoverTrigger className="h-9 rounded-md border px-3 text-sm font-medium outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50">
Manage
</PopoverTrigger>
<PopoverContent label="Share settings" align="end" showCloseButton>
<PopoverHeader>
<PopoverTitle>Share settings</PopoverTitle>
<PopoverDescription>
Choose who can open this project.
</PopoverDescription>
</PopoverHeader>
<div className="grid gap-2">
<button
type="button"
onClick={() => setAccess("Team only")}
className="flex h-9 items-center gap-2 rounded-md px-2 text-start text-sm outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<LockKeyhole aria-hidden="true" className="size-4" /> Team only{" "}
{access === "Team only" && (
<span className="ms-auto text-xs text-primary">Selected</span>
)}
</button>
<button
type="button"
onClick={() => setAccess("Anyone with link")}
className="flex h-9 items-center gap-2 rounded-md px-2 text-start text-sm outline-none hover:bg-accent focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<Link2 aria-hidden="true" className="size-4" /> Anyone with link{" "}
{access === "Anyone with link" && (
<span className="ms-auto text-xs text-primary">Selected</span>
)}
</button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/popoverInstall the dependencies.
$ pnpm add radix-ui lucide-reactAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/i18nCopy the source into your project.
components/ballmac/popover.tsx// Ballmac UI: Popover. https://ui.ballmac.com/components/popover "use client"; import * as React from "react"; import { X } from "lucide-react"; import { Popover as Primitive } from "radix-ui"; import { cn } from "@/lib/utils"; import { useMessages } from "@/lib/ballmac/i18n"; type PopoverProps = React.ComponentProps<typeof Primitive.Root>; function Popover(props: PopoverProps) { return <Primitive.Root data-slot="popover" {...props} />; } type PopoverTriggerProps = React.ComponentProps<typeof Primitive.Trigger>; function PopoverTrigger({ className, ...props }: PopoverTriggerProps) { return ( <Primitive.Trigger data-slot="popover-trigger" className={className} {...props} /> ); } type PopoverAnchorProps = React.ComponentProps<typeof Primitive.Anchor>; function PopoverAnchor(props: PopoverAnchorProps) { return <Primitive.Anchor data-slot="popover-anchor" {...props} />; } type PopoverContentProps = React.ComponentProps<typeof Primitive.Content> & { /** Accessible name of the interactive panel. */ label: string; /** Show the close control in the top-right corner. */ showCloseButton?: boolean; /** Accessible name for the close control. */ closeLabel?: string; }; function PopoverContent({ className, children, label, align = "center", sideOffset = 8, showCloseButton = false, closeLabel, ...props }: PopoverContentProps) { const msg = useMessages() closeLabel ??= msg("popover.closeLabel", "Close popover") return ( <Primitive.Portal> <Primitive.Content data-slot="popover-content" aria-label={label} align={align} sideOffset={sideOffset} collisionPadding={12} className={cn( "z-50 grid w-[min(21rem,calc(100vw-1.5rem))] gap-3 rounded-xl border bg-popover p-4 text-popover-foreground shadow-[0_12px_36px_-10px_rgb(0_0_0/0.2)] outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0 data-[state=open]:zoom-in-95 data-[state=closed]:zoom-out-95 motion-reduce:animate-none", className, )} {...props} > {children} {showCloseButton && ( <Primitive.Close data-slot="popover-close" aria-label={closeLabel} className="absolute top-2.5 end-2.5 inline-flex size-8 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50" > <X aria-hidden="true" className="size-4" /> </Primitive.Close> )} </Primitive.Content> </Primitive.Portal> ); } type PopoverHeaderProps = React.ComponentProps<"div">; function PopoverHeader({ className, ...props }: PopoverHeaderProps) { return ( <div data-slot="popover-header" className={cn("grid gap-1 pe-6", className)} {...props} /> ); } type PopoverTitleProps = React.ComponentProps<"h3">; function PopoverTitle({ className, ...props }: PopoverTitleProps) { return ( <h3 data-slot="popover-title" className={cn("text-sm font-semibold", className)} {...props} /> ); } type PopoverDescriptionProps = React.ComponentProps<"p">; function PopoverDescription({ className, ...props }: PopoverDescriptionProps) { return ( <p data-slot="popover-description" className={cn("text-sm leading-relaxed text-muted-foreground", className)} {...props} /> ); } export { Popover, PopoverTrigger, PopoverAnchor, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription, type PopoverProps, type PopoverTriggerProps, type PopoverAnchorProps, type PopoverContentProps, type PopoverHeaderProps, type PopoverTitleProps, type PopoverDescriptionProps, };Update the import paths to match your project setup.
Usage
import { Popover, PopoverTrigger, PopoverAnchor, PopoverContent, PopoverHeader, PopoverTitle, PopoverDescription } from "@/components/ballmac/popover"The full example is in the Code tab above.
Examples
Profile details
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/components/ballmac/popover";
export default function PopoverStates() {
return (
<div className="w-full max-w-xs">
<Popover>
<PopoverTrigger className="h-9 rounded-md bg-primary px-3 text-sm font-medium text-primary-foreground outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50">
Edit display name
</PopoverTrigger>
<PopoverContent label="Display name" align="start" showCloseButton>
<PopoverHeader>
<PopoverTitle>Display name</PopoverTitle>
<PopoverDescription>
This name appears to your team.
</PopoverDescription>
</PopoverHeader>
<label className="grid gap-1 text-xs font-medium">
Name
<input
defaultValue="Alex Morgan"
className="h-9 rounded-md border bg-background px-3 text-sm outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
/>
</label>
</PopoverContent>
</Popover>
</div>
);
}API reference
| Prop | Type | Default |
|---|---|---|
label*Accessible name of the interactive panel. | string | — |
showCloseButtonShow the close control in the top-right corner. | boolean | false |
closeLabelAccessible name for the close control. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Enter / Space | Opens the panel from its trigger |
| Tab / Shift+Tab | Moves among panel controls |
| Escape | Closes and returns focus to the trigger |
Use with AI
Displays a small interactive panel anchored to a control, with managed focus and Escape dismissal. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Popover (@ballmac/popover) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Inline editors and compact preferences
- Actions needing more room than a menu
Not for
- Long or critical forms; use dialog
- Noninteractive link previews; use hover-card
Registry JSON: https://ui.ballmac.com/r/popover.json
Credits
Free to use in personal and commercial projects.
- Registry
- @ballmac/i18nshadcn/utils
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Input
A text input in three heights that match Button, plus InputGroup and InputGroupAddon for leading or trailing icons, units and domains.
Accordion
Vertically stacked disclosure sections on Radix Accordion, single or multiple open, with hairline dividers, a rotating plus and height animation.
Avatar
A Radix avatar with image and initials fallback, three sizes, an optional presence dot, and AvatarGroup for overlapping stacks with a +N overflow counter.