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 the right affordance whenever a click should open a short floating panel anchored to its trigger — a share menu, an info panel, a form-field hint, a profile dropdown. The Popover component in Drivn renders a <div className="relative inline-flex"> root, a trigger button styled as the Drivn outline button, and a content panel positioned to one of four sides (top, bottom, left, right). The whole tree lives in @/components/ui/popover.tsx, owns its open state through React.useState, and dismisses on outside click through a mousedown listener on document. There is no Radix dependency, no portal, and no focus trap.

This page collects the call-site shapes that ship in real projects. Every example imports the component from @/components/ui/popover — the path the Drivn CLI installs under — and uses the dot-notation API of Popover, Popover.Trigger, and Popover.Content. Every snippet is TypeScript because Drivn ships TypeScript-only.

The examples below cover a default bottom-anchored popover, an info popover positioned to the right of an icon button, a placement-switching example that demonstrates all four sides, a share menu with copy and social actions, and a form-field hint that opens next to a label. The full component reference and props table live on the Popover docs page, and the shadcn/ui comparison sits on Drivn vs shadcn Popover. For a fresh install, see Installation and the Drivn CLI docs.

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

For a layout demo or a settings panel that lets the user pick where a popover opens, render four Popover roots side by side with each position value. The component re-renders cleanly when the position prop changes because the value flows through the context to Popover.Content, which reads styles.positions[position] and applies the matching placement class. Wrap the four triggers in a flex items-center gap-3 row so the buttons sit in a tidy strip and the four popovers open in their distinct directions when clicked.

This shape doubles as a visual-regression test fixture — open each placement in turn and confirm the panel anchors to the expected side of the trigger. The default min-w-[200px] width means the right and left placements occupy a similar horizontal footprint to the top and bottom placements, so the four panels look balanced when compared. Pair the demo with the Popover docs page when teaching the placement model in a design system handbook.

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.