Skip to content
Drivn
7 min read

React Popover Component Examples

Copy-paste React Popover examples: share menu, info panel, form field hint, profile menu, and four-side placement. Zero runtime deps. TypeScript only.

A popover is what you reach for when a click should open a small floating panel pinned to the thing that was clicked — a share menu, a field hint, a build-details panel, a profile menu. Drivn's Popover does that in one file, @/components/ui/popover.tsx, and nothing else. The root renders a relative inline-flex div, holds open in React.useState, and puts { open, setOpen, position } on a context that the trigger and the content both read. Outside-click dismissal is a mousedown listener on document that closes when the event target falls outside the root ref. There is no Radix package, no portal, and no focus trap.

The content panel is always in the DOM. Closed, it carries opacity-0 scale-95 pointer-events-none; open, those swap for opacity-100 scale-100, and a transition-[opacity,scale] duration-150 ease-out on the base class handles the fade. Placement is a position prop with four values that map to a styles.positions entry — fixed at render, with no collision flipping.

The examples below cover a default bottom-anchored panel, an info panel pinned to the right of a trigger, a demo of all four placements, a share menu with copy and social actions, and a form-field hint beside a label. Every one imports from @/components/ui/popover, the path the Drivn CLI writes to, and uses the Popover / Popover.Trigger / Popover.Content dot-notation API. All of it is TypeScript, because that is the only thing Drivn ships. Start with Installation if the component is not in your project yet, or read Drivn vs shadcn Popover for the Radix comparison.

Default bottom-anchored popover

The shortest possible Popover. Pass children to Popover.Trigger to render the outline button text, then pass content to Popover.Content to render the floating panel. The default position is bottom, so the panel anchors top-full mt-2 left-1/2 -translate-x-1/2 relative to the root — eight pixels below the trigger and horizontally centered on it. The panel ships with min-w-[200px] p-4 bg-card border border-border rounded-[12px] shadow-lg so a short heading plus a sentence of body text fits without extra wrapper styling.

This is the right starting shape for a brief help panel, a confirmation prompt with no input fields, or any short content that fits in a 200-pixel-wide panel without scrolling. The trigger inherits the Drivn outline button styling — rounded corners, muted border, hover background — so it sits next to other outline buttons in a toolbar without visual mismatch. For panels that need a different trigger element (an avatar, an icon-only button, a link), see the form-field hint example later on this page or read Drivn vs shadcn Popover for the trigger-customization story.

1import { Popover } from "@/components/ui/popover"
2
3export default function HelpPopover() {
4 return (
5 <Popover>
6 <Popover.Trigger>What's this?</Popover.Trigger>
7 <Popover.Content>
8 <p className="font-semibold mb-1">Quick help</p>
9 <p className="text-sm text-muted-foreground">
10 The status badge reflects the latest deploy. Hover for the deploy ID.
11 </p>
12 </Popover.Content>
13 </Popover>
14 )
15}

Side-anchored info panel

Set position="right" on the root to anchor the content to the right of the trigger. The panel slides to left-full ml-2 top-1/2 -translate-y-1/2 — eight pixels to the right of the trigger, vertically centered on it. The right placement is the natural fit when the trigger sits at the start of a row and the content needs the horizontal space the row already provides. Use top for triggers at the bottom of the viewport and left when the trigger sits at the right edge of a container.

Drivn's placement is fixed at render time — the panel does not flip to the opposite side when it would collide with the viewport edge. The intentional simplification means triggers placed too close to a viewport edge will render content that overflows the viewport. The fix is to pick the placement at the call site based on where the trigger sits, or to switch to the shadcn/ui flavor for Floating-UI-driven collision flipping. See the placement comparison in Drivn vs shadcn Popover for the full trade-off discussion.

1import { Popover } from "@/components/ui/popover"
2
3export default function SideInfoPopover() {
4 return (
5 <Popover position="right">
6 <Popover.Trigger>Details</Popover.Trigger>
7 <Popover.Content>
8 <p className="font-semibold mb-1">Build details</p>
9 <p className="text-sm text-muted-foreground">
10 Last successful build ran for 42 seconds across 14 jobs.
11 </p>
12 </Popover.Content>
13 </Popover>
14 )
15}

All four placements side by side

When you need to see the placement model rather than read about it, render four roots in a row, one per position value. Each Popover.Content reads position off the context and looks it up in styles.positions, which holds exactly four entries: top is bottom-full mb-2 left-1/2 -translate-x-1/2, bottom is top-full mt-2 left-1/2 -translate-x-1/2, left is right-full mr-2 top-1/2 -translate-y-1/2, and right is left-full ml-2 top-1/2 -translate-y-1/2. Side by side the symmetry is obvious — vertical placements center horizontally and offset with mb-2 or mt-2, horizontal placements center vertically and offset with mr-2 or ml-2.

The demo doubles as a regression fixture. Open each trigger in turn and confirm the panel lands on the expected side; because the panel is always mounted and only toggles opacity, a placement bug shows up as a misplaced ghost before you even click. Keep the row in a flex items-center gap-3 wrapper and leave the strip some breathing room — the left and right panels each claim at least 200 pixels beside their trigger, enough to overlap a neighbour in a tight container. The Popover docs page carries the props table if you are documenting the placement model for a team.

1import { Popover } from "@/components/ui/popover"
2
3export default function PlacementDemo() {
4 return (
5 <div className="flex items-center gap-3">
6 {(['top', 'bottom', 'left', 'right'] as const).map((side) => (
7 <Popover key={side} position={side}>
8 <Popover.Trigger>{side}</Popover.Trigger>
9 <Popover.Content>
10 <p className="text-sm">Anchored to {side}.</p>
11 </Popover.Content>
12 </Popover>
13 ))}
14 </div>
15 )
16}

Share menu with copy and social actions

A typical share menu lives behind a single "Share" button and opens a panel with two or three actions: copy the current URL, share to X, share by email. Render the actions as buttons inside the Popover.Content, each calling the matching handler. The popover dismisses on outside click automatically, so after the user clicks "Copy link" the panel closes the next time the user clicks anywhere else — which matches the expected behaviour for a transient share menu.

For a polished UX, swap the plain action buttons for Drivn Button instances with variant="ghost" so the actions inherit the same hover-background and rounded-corner styling as the trigger. Add a text-left w-full className on each button so the labels left-align inside the panel. If the panel grows wider than the default min-w-[200px] because of long action labels, the panel inherits the trigger's center alignment — the left-1/2 -translate-x-1/2 math centers any width automatically. See the Drivn vs shadcn Popover page for the trigger-customization trade-off discussion that applies to the share-menu shape.

1'use client'
2import { Popover } from "@/components/ui/popover"
3import { Button } from "@/components/ui/button"
4import { Link as LinkIcon, Twitter, Mail } from "lucide-react"
5
6export default function ShareMenu({ url }: { url: string }) {
7 const copy = () => navigator.clipboard.writeText(url)
8 const share = () =>
9 window.open(`https://x.com/intent/tweet?url=${encodeURIComponent(url)}`)
10 const mail = () => (window.location.href = `mailto:?body=${url}`)
11
12 return (
13 <Popover>
14 <Popover.Trigger>Share</Popover.Trigger>
15 <Popover.Content className="min-w-[220px]">
16 <p className="font-semibold mb-2">Share this page</p>
17 <div className="flex flex-col gap-1">
18 <Button variant="ghost" leftIcon={LinkIcon} onClick={copy}>
19 Copy link
20 </Button>
21 <Button variant="ghost" leftIcon={Twitter} onClick={share}>
22 Share to X
23 </Button>
24 <Button variant="ghost" leftIcon={Mail} onClick={mail}>
25 Email
26 </Button>
27 </div>
28 </Popover.Content>
29 </Popover>
30 )
31}

Form-field hint popover

A common form pattern is a label with an info icon next to it that opens a popover explaining the field. Place the Popover next to the Input inside the same <label> block, render the trigger as the info icon with aria-label="Field info", and let the content explain validation rules, accepted formats, or examples. The pattern keeps the inline help available without permanently consuming layout space below the input — readers who already know the field skip past the icon, readers who need help click it.

Use position="right" so the content opens to the right of the icon rather than below it, which avoids covering the input on a focused field. For mobile layouts where horizontal space is tighter, switch to position="top" so the panel stacks above the field instead. The popover content can include a short example or a code snippet inside a <code> tag — keep the panel narrow (the default 200-pixel min-width is usually enough) so the inline help reads as a footnote rather than competing with the form layout. Pair the popover with the Drivn Input and Label components for the rest of the field wiring.

1'use client'
2import { Popover } from "@/components/ui/popover"
3import { Input } from "@/components/ui/input"
4import { Info } from "lucide-react"
5
6export default function ApiKeyField() {
7 return (
8 <label className="block">
9 <div className="flex items-center gap-2 mb-1">
10 <span className="text-sm font-medium">API key</span>
11 <Popover position="right">
12 <Popover.Trigger aria-label="Field info" className="p-1">
13 <Info className="h-4 w-4" />
14 </Popover.Trigger>
15 <Popover.Content>
16 <p className="text-sm">
17 Starts with <code>sk_</code> followed by 32 characters. Copy from
18 the dashboard under Settings → API.
19 </p>
20 </Popover.Content>
21 </Popover>
22 </div>
23 <Input placeholder="sk_..." />
24 </label>
25 )
26}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. The component owns open state through React.useState, attaches a mousedown listener to document through React.useEffect, and shares the state through a React.createContext value — all three are client-only APIs. The first line of the registry source is 'use client' for exactly this reason. The call sites that import Popover are free to live in Server Components, since the 'use client' boundary is declared inside the popover file itself rather than at the call site.

The cleanest path is to expose a controlled mode by lifting the open state to the parent. Edit @/components/ui/popover.tsx to accept open and onOpenChange props on PopoverRoot, fall back to the internal useState when the props are undefined, and pass open and setOpen through the context. The parent then owns the boolean and can flip it to false from any handler — for example, after a successful copy-to-clipboard action inside the share menu. The edit is roughly ten lines and follows the same pattern as Drivn Dialog.

Drivn renders the content panel inline rather than through a portal. The panel is positioned with absolute z-50 relative to the root, which means any ancestor with overflow: hidden will clip the panel at its boundary. The two fixes are to move the popover outside the clipping ancestor (typically by lifting it to the same level as the card rather than inside it) or to switch the ancestor to overflow-visible for the duration the popover is open. shadcn/ui's Popover sidesteps the issue through a Radix portal, which is the right call when the popover must escape a card or modal stack.

The default Popover.Trigger renders the Drivn Button component with variant="outline". To use a different element, edit the Trigger function inside @/components/ui/popover.tsx and replace the <Button> JSX with the element you want — a plain <button> for an icon-only trigger, a Drivn Avatar for a profile menu, a <span> with a hover-underline for an inline-help affordance. The usePopover() hook still works inside the new trigger because the context lives at the component file level rather than inside the Button wrapper.

No — the panel is click-driven. Clicking the trigger toggles the open flag, clicking outside the root closes the panel, and the Popover.Content does not respond to pointer-enter or pointer-leave events. For a hover-driven floating panel, use the Drivn Tooltip component instead, which is designed for the short hover-hint case. Mixing click and hover triggers on the same affordance tends to confuse keyboard users, so picking one model per call site is the cleaner pattern.