import { Popover, Button } from "@cloudflare/kumo";
import { BellIcon } from "@phosphor-icons/react";
export function PopoverHeroDemo() {
return (
<Popover.Root>
<Popover.Trigger
render={
<Button shape="square" icon={BellIcon} aria-label="Notifications" />
}
/>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Notifications</Popover.Title>
<Popover.Description>
You are all caught up. Good job!
</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}Installation
Barrel
import { Popover } from "@cloudflare/kumo";Granular
import { Popover } from "@cloudflare/kumo/components/popover";Usage
import { Popover, Button } from "@cloudflare/kumo";
export default function Example() {
return (
<Popover.Root>
<Popover.Trigger render={<Button />}>Open</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Popover Title</Popover.Title>
<Popover.Description>Popover content goes here.</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}Popover vs Tooltip
While popovers can be triggered on hover (using openOnHover), they serve a
different purpose than tooltips. Understanding when to use each is important
for accessibility and user experience.
| Tooltip | Popover | |
|---|---|---|
| Purpose | Short, non-interactive text labels for identification | Rich, interactive content containers |
| Content | Plain text only | Any content: links, buttons, forms, images |
| Trigger | Hover or focus | Click (default) or hover |
| ARIA Role |
|
|
| Keyboard | Not focusable | Focus moves inside, traps when open |
Use a Tooltip when you need to label an icon button or provide a brief explanation. Use a Popover when users need to interact with the content inside, such as clicking links, filling out forms, or dismissing with a button.
Examples
Basic Popover
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverBasicDemo() {
return (
<Popover.Root>
<Popover.Trigger render={<Button />}>Open Popover</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Popover Title</Popover.Title>
<Popover.Description>
This is a basic popover with a title and description.
</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}Deprecated Content compatibility
Popover.Content is deprecated, but remains available so existing popovers do
not break. It combines Portal, Positioner, Popup, and Arrow into one
component and fades in smoothly using opacity only, without a scale or
transform animation. New popovers should compose the individual parts as
shown in the basic example.
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverLegacyContentDemo() {
return (
<Popover>
<Popover.Trigger render={<Button variant="secondary" />}>
Open legacy Content
</Popover.Trigger>
<Popover.Content className="w-72">
<Popover.Title>Compatibility mode</Popover.Title>
<Popover.Description>
This legacy wrapper fades in without scaling or transforming.
</Popover.Description>
</Popover.Content>
</Popover>
);
}Scrollable content
When the arrow and heading should remain fixed, place the overflowing content
in an inner scroll container. Existing overflow styles on the deprecated
Popover.Content remain supported by its fade-only transition.
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverOverflowDemo() {
return (
<Popover.Root>
<Popover.Trigger render={<Button variant="secondary" />}>
Open scrollable Popover
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup className="w-72">
<Popover.Arrow />
<Popover.Title>Recent notifications</Popover.Title>
<Popover.Description>
The list scrolls while the heading and arrow stay in place.
</Popover.Description>
<div
aria-label="Recent notifications"
className="mt-3 max-h-40 overflow-y-auto rounded-md border border-kumo-hairline outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand"
tabIndex={0}
>
{Array.from({ length: 10 }, (_, index) => (
<div
key={index}
className="border-b border-kumo-hairline px-3 py-2 last:border-b-0"
>
Notification {index + 1}
</div>
))}
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}CSS variables
Base UI exposes the space between the trigger and the viewport edge as
--available-height. This example gives the popup a preferred height of
320px, then uses the variable as its maximum height so the popup shrinks and
its inner content scrolls when less space is available.
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverCssVariablesDemo() {
return (
<Popover.Root>
<Popover.Trigger render={<Button />}>Open tall Popover</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup className="h-80 max-h-[var(--available-height)] w-72">
<Popover.Arrow />
<Popover.Title>Available-height popup</Popover.Title>
<Popover.Description>
The popup is 320px tall unless the viewport provides less space.
</Popover.Description>
<div
aria-label="Popover rows"
className="mt-3 min-h-0 flex-1 overflow-y-auto rounded-md border border-kumo-hairline outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand"
tabIndex={0}
>
{Array.from({ length: 12 }, (_, index) => (
<div
key={index}
className="border-b border-kumo-hairline px-3 py-2 last:border-b-0"
>
Row {index + 1}
</div>
))}
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}With Close Button
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverWithCloseDemo() {
return (
<Popover.Root>
<Popover.Trigger render={<Button />}>Open Settings</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Settings</Popover.Title>
<Popover.Description>
Configure your preferences below.
</Popover.Description>
<div className="mt-3">
<Popover.Close render={<Button variant="secondary" size="sm" />}>
Close
</Popover.Close>
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}Positioning
Use the side prop on Popover.Positioner to control where the popover
appears relative to the trigger.
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverPositionDemo() {
return (
<div className="flex flex-wrap gap-4">
<Popover.Root>
<Popover.Trigger render={<Button variant="secondary" />}>
Bottom
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="bottom">
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Bottom</Popover.Title>
<Popover.Description>
Popover on bottom (default).
</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
<Popover.Root>
<Popover.Trigger render={<Button variant="secondary" />}>
Top
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="top">
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Top</Popover.Title>
<Popover.Description>Popover on top.</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
<Popover.Root>
<Popover.Trigger render={<Button variant="secondary" />}>
Left
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="left">
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Left</Popover.Title>
<Popover.Description>Popover on left.</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
<Popover.Root>
<Popover.Trigger render={<Button variant="secondary" />}>
Right
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner side="right">
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Right</Popover.Title>
<Popover.Description>Popover on right.</Popover.Description>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
</div>
);
}Custom Content
Popovers can contain any content, including custom layouts with avatars, buttons, and more.
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverCustomContentDemo() {
return (
<Popover.Root>
<Popover.Trigger render={<Button />}>User Profile</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup className="w-64">
<Popover.Arrow />
<div className="flex items-center gap-3">
<div className="size-10 rounded-full bg-kumo-recessed" />
<div>
<Popover.Title>Jane Doe</Popover.Title>
<p className="text-sm text-kumo-subtle">jane@example.com</p>
</div>
</div>
<div className="mt-3 flex gap-2 border-t border-kumo-hairline pt-3">
<Button variant="secondary" size="sm" className="flex-1">
Profile
</Button>
<Popover.Close
render={<Button variant="ghost" size="sm" className="flex-1" />}
>
Sign Out
</Popover.Close>
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}Open on Hover
Use openOnHover on the trigger to open the popover when the user hovers over
it. You can also specify a delay in milliseconds before the popover appears.
import { Popover, Button } from "@cloudflare/kumo";
export function PopoverOpenOnHoverDemo() {
return (
<Popover.Root>
<Popover.Trigger
openOnHover
delay={200}
render={<Button variant="secondary" />}
>
Hover Me
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>Hover Triggered</Popover.Title>
<Popover.Description>
This popover opens on hover with a 200ms delay. It can still
contain interactive content like buttons and links.
</Popover.Description>
<div className="mt-3">
<Popover.Close render={<Button variant="secondary" size="sm" />}>
Got it
</Popover.Close>
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}Virtual Anchor
Use the anchor prop on Popover.Positioner to position the popover against
an element other than the trigger, or against a virtual point (e.g., a
DOMRect from getBoundingClientRect()). This is useful when the trigger and
the desired anchor are in different component trees.
| Name | Status | |
|---|---|---|
| api-gateway | Active | |
| auth-service | Active | |
| worker-prod | Paused |
import { useState, useRef } from "react";
import { Popover, Button } from "@cloudflare/kumo";
import { DotsThree } from "@phosphor-icons/react";
/** Popover anchored to a virtual element instead of a trigger. */
export function PopoverVirtualAnchorDemo() {
const [selectedRow, setSelectedRow] = useState<string | null>(null);
const [anchorRect, setAnchorRect] = useState<DOMRect | null>(null);
const rowRefs = useRef<Map<string, HTMLTableRowElement>>(new Map());
const rows = [
{ id: "1", name: "api-gateway", status: "Active" },
{ id: "2", name: "auth-service", status: "Active" },
{ id: "3", name: "worker-prod", status: "Paused" },
];
const handleEdit = (id: string) => {
const row = rowRefs.current.get(id);
if (row) {
setAnchorRect(row.getBoundingClientRect());
setSelectedRow(id);
}
};
return (
<div className="w-full">
<div className="overflow-hidden rounded-lg border border-kumo-hairline">
<table className="w-full text-sm">
<thead className="bg-kumo-elevated">
<tr>
<th className="px-4 py-2 text-left font-medium">Name</th>
<th className="px-4 py-2 text-left font-medium">Status</th>
<th className="w-12 px-4 py-2"></th>
</tr>
</thead>
<tbody className="divide-y divide-kumo-hairline">
{rows.map((row) => (
<tr
key={row.id}
ref={(el) => {
if (el) rowRefs.current.set(row.id, el);
}}
className={
selectedRow === row.id ? "bg-kumo-recessed" : "bg-kumo-base"
}
>
<td className="px-4 py-2 font-mono">{row.name}</td>
<td className="px-4 py-2 text-kumo-subtle">{row.status}</td>
<td className="px-4 py-2">
<Button
size="xs"
variant="ghost"
shape="square"
icon={DotsThree}
aria-label={`Actions for ${row.name}`}
onClick={() => handleEdit(row.id)}
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
<Popover.Root
open={!!selectedRow}
onOpenChange={(open) => !open && setSelectedRow(null)}
>
<Popover.Portal>
<Popover.Positioner
side="left"
anchor={
anchorRect
? { getBoundingClientRect: () => anchorRect }
: undefined
}
>
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>
Edit {rows.find((r) => r.id === selectedRow)?.name}
</Popover.Title>
<Popover.Description>
The popover anchors to the selected row, not the icon button.
</Popover.Description>
<div className="mt-3">
<Popover.Close
render={<Button size="sm" variant="secondary" />}
>
Close
</Popover.Close>
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
</div>
);
}API Reference
Popover.Root
The root component that manages the popover’s open state.
| Prop | Type | Default | Description |
|---|---|---|---|
| side | "top" | "bottom" | "left" | "right" | "bottom" | Which side of the trigger the popover appears on. - `"top"` — Above the trigger - `"bottom"` — Below the trigger - `"left"` — Left of the trigger - `"right"` — Right of the trigger |
Popover.Trigger
A button that opens the popover when clicked. Use render to render your own
element.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Portal
Renders the popover outside the application DOM hierarchy. It uses the
container from KumoPortalProvider by default and accepts a container prop
to override it.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Backdrop
An optional backdrop rendered behind the popover.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Positioner
Positions the popup relative to its trigger or an anchor. Use side,
align, and their offset props to control placement.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Popup
The styled popup container. Place Popover.Arrow and the popover content
directly inside it.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Arrow
An optional arrow that points toward the popover’s anchor.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Viewport
A viewport for animating content changes when one popup is shared by multiple triggers. It is not required for ordinary popovers or scrolling content.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Content
A deprecated compatibility wrapper that combines Portal, Positioner,
Popup, and Arrow. Existing usages remain supported, but new code should
compose the individual parts shown above.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Title
A heading that labels the popover for accessibility.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Description
A paragraph providing additional context about the popover content.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.
Popover.Close
A button that closes the popover when clicked. Use render to render your own
element.
| Prop | Type | Default |
|---|
No component-specific props. Accepts standard HTML attributes.