ComponentsUpdated Sep 9, 2026
Action Popover
A hover toolbar for list items. Opens 250ms after the pointer enters the trigger, closes 350ms after it leaves, and never steals focus from the content beneath.
Source: packages/ui/src/components/action-popover
import { ActionPopover } from '@highlight/ui'Change the props; the preview and the snippet update together.
Design sync recap
Props
Pin open
Side
Align
-12
12
Archive action
<ActionPopover open={open}> <ActionPopover.Trigger className="w-80"> <Cell … /> </ActionPopover.Trigger> <ActionPopover.Content side="top" sideOffset={-12} alignOffset={12}> <ActionPopover.Body> <ActionPopover.Screen id="recap" key="actions" isActive> <ActionPopover.Button left={<Copy />}>Copy</ActionPopover.Button> <ActionPopover.Button left={<Share />}>Share</ActionPopover.Button> <ActionPopover.Separator /> <ActionPopover.Button aria-label="Archive" shape="circle"><Archive /></ActionPopover.Button> </ActionPopover.Screen> </ActionPopover.Body> </ActionPopover.Content></ActionPopover>Hover the cell. Actions always live inside a Screen, which is the flex row. Content itself does not lay its children out.
Design sync recap
import { Archive, Calendar, Copy, Share } from 'lucide-react'import { ActionPopover, Cell } from '@highlight/ui' export default function ActionPopoverBasic() { return ( <ActionPopover> <ActionPopover.Trigger className="w-80"> <Cell onClick={() => {}}> <Cell.Header> <Cell.Icon> <Calendar /> </Cell.Icon> <Cell.Title>Design sync recap</Cell.Title> </Cell.Header> </Cell> </ActionPopover.Trigger> <ActionPopover.Content align="end" side="top" sideOffset={-12} alignOffset={12}> <ActionPopover.Body> <ActionPopover.Screen id="recap" key="actions" isActive> <ActionPopover.Button left={<Copy />}>Copy</ActionPopover.Button> <ActionPopover.Button left={<Share />}>Share</ActionPopover.Button> <ActionPopover.Separator /> <ActionPopover.Button aria-label="Archive" shape="circle"> <Archive /> </ActionPopover.Button> </ActionPopover.Screen> </ActionPopover.Body> </ActionPopover.Content> </ActionPopover> )}Body crossfades between Screens as isActive flips, and the toolbar resizes to the new content. This is how a destructive action asks for confirmation in place.
Design sync recap
import { useState } from 'react'import { Calendar, Trash2, X } from 'lucide-react'import { ActionPopover, Cell } from '@highlight/ui' export default function ActionPopoverScreens() { const [confirming, setConfirming] = useState(false) return ( <ActionPopover> <ActionPopover.Trigger className="w-80"> <Cell onClick={() => {}}> <Cell.Header> <Cell.Icon> <Calendar /> </Cell.Icon> <Cell.Title>Design sync recap</Cell.Title> </Cell.Header> </Cell> </ActionPopover.Trigger> <ActionPopover.Content align="end" side="top" sideOffset={-12} alignOffset={12}> <ActionPopover.Body> <ActionPopover.Screen id="recap" key="actions" isActive={!confirming}> <ActionPopover.Button left={<Trash2 />} onClick={() => setConfirming(true)}> Delete </ActionPopover.Button> </ActionPopover.Screen> <ActionPopover.Screen id="recap" key="confirm" isActive={confirming}> <span className="text-foreground-secondary whitespace-nowrap px-2 text-sm leading-none"> Delete this recap? </span> <ActionPopover.Button variant="primary" tone="danger" left={<Trash2 />} onClick={() => setConfirming(false)} > Delete </ActionPopover.Button> <ActionPopover.Button aria-label="Cancel" shape="circle" onClick={() => setConfirming(false)}> <X /> </ActionPopover.Button> </ActionPopover.Screen> </ActionPopover.Body> </ActionPopover.Content> </ActionPopover> )}The root, then any compound sub-components. Every part accepts className and forwards the rest of its props to the element or Base UI part beneath it.
ActionPopover- Base UI Popover.Root. Owns open state; holds the trigger and the pill toolbar.
ActionPopover.Trigger- A div wrapper; hover on it opens the toolbar.
ActionPopover.Content- Portal + Positioner + Popup with a Toolbar root. align, side, sideOffset, alignOffset, collision props. Put a Body inside, not buttons.
ActionPopover.Body- AnimatePresence wrapper around the Screens. mode defaults to popLayout.
ActionPopover.Screen- One toolbar state, and the flex row its children sit in. id, key, isActive; only the active one renders.
ActionPopover.Button- Toolbar button built on Button. size defaults to 'sm', shape to 'rounded'.
ActionPopover.Group / Separator- Optional structure within a Screen.
- open and onOpenChange make it controlled; clicks inside do not toggle it.
- Content renders a Toolbar root with no layout of its own, so buttons placed directly inside it stack vertically. Always wrap them in Body + Screen.