Skip to content
Highlight

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.

Import

Source: packages/ui/src/components/action-popover

tsx
import { ActionPopover } from '@highlight/ui'

Basic

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
tsx
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>
  )
}

Multiple screens

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
tsx
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/80 whitespace-nowrap px-2 text-sm leading-none">Delete this recap?</span>
            <ActionPopover.Button variant="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>
  )
}

Parts

Compound sub-components. Every part accepts className and forwards the rest of its props to Base UI.

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 'small', 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.