Skip to main content
Markdown

Popover

Popover displays a floating panel on click or hover to surface additional content, forms, or actions. It wraps the Spar headless Popover primitive and adds Takeoff visual vocabulary — variant, close button, and arrow slots.

Usage

import { Popover } from '@takeoff-ui/react-spar';
<Popover>
<Popover.Trigger />
<Popover.Content>
<Popover.Header />
<Popover.Description />
<Popover.Arrow />
<Popover.Close />
</Popover.Content>
</Popover>

Playground


function PlaygroundDemo() {
  return (
    <Popover>
      <Popover.Trigger as={Button}>Click me</Popover.Trigger>
      <Popover.Content>
        <Popover.Header>Title</Popover.Header>
        <Popover.Description>Popover content goes here.</Popover.Description>
      </Popover.Content>
    </Popover>
  );
}

render(<PlaygroundDemo />);

Header & Description

Use Popover.Header and Popover.Description to structure the content with a title and supporting text. Both are styling slots — no behavior is attached.


function HeaderDescriptionDemo() {
  return (
    <Popover>
      <Popover.Trigger as={Button}>Open</Popover.Trigger>
      <Popover.Content>
        <Popover.Header>Account settings</Popover.Header>
        <Popover.Description>Manage your personal information, password, and notifications.</Popover.Description>
      </Popover.Content>
    </Popover>
  );
}

render(<HeaderDescriptionDemo />);

Variants

The variant prop controls the visual appearance of the popover content.


function TypesDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Popover>
        <Popover.Trigger as={Button}>White</Popover.Trigger>
        <Popover.Content variant="white">
          <Popover.Description>White variant popover</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Dark</Popover.Trigger>
        <Popover.Content variant="dark">
          <Popover.Description>Dark variant popover</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Info</Popover.Trigger>
        <Popover.Content variant="info">
          <Popover.Description>Info variant popover</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Success</Popover.Trigger>
        <Popover.Content variant="success">
          <Popover.Description>Success variant popover</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Warning</Popover.Trigger>
        <Popover.Content variant="warning">
          <Popover.Description>Warning variant popover</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Danger</Popover.Trigger>
        <Popover.Content variant="danger">
          <Popover.Description>Danger variant popover</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Neutral</Popover.Trigger>
        <Popover.Content variant="neutral">
          <Popover.Description>Neutral variant popover</Popover.Description>
        </Popover.Content>
      </Popover>
    </div>
  );
}

render(<TypesDemo />);

Placement


function PlacementDemo() {
  return (
    <div className="flex flex-wrap justify-center gap-3">
      <Popover>
        <Popover.Trigger as={Button}>Top</Popover.Trigger>
        <Popover.Content side="top">
          <Popover.Description>Positioned top</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Bottom</Popover.Trigger>
        <Popover.Content side="bottom">
          <Popover.Description>Positioned bottom</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Left</Popover.Trigger>
        <Popover.Content side="left">
          <Popover.Description>Positioned left</Popover.Description>
        </Popover.Content>
      </Popover>
      <Popover>
        <Popover.Trigger as={Button}>Right</Popover.Trigger>
        <Popover.Content side="right">
          <Popover.Description>Positioned right</Popover.Description>
        </Popover.Content>
      </Popover>
    </div>
  );
}

render(<PlacementDemo />);

Arrow


function ArrowDemo() {
  return (
    <Popover>
      <Popover.Trigger as={Button}>With arrow</Popover.Trigger>
      <Popover.Content>
        <Popover.Description>Popover with arrow</Popover.Description>
        <Popover.Arrow />
      </Popover.Content>
    </Popover>
  );
}

render(<ArrowDemo />);

Close Button


function CloseDemo() {
  return (
    <Popover>
      <Popover.Trigger as={Button}>Open</Popover.Trigger>
      <Popover.Content>
          <Popover.Header className="flex items-center justify-between">
          <span>Title</span>
          <Popover.Close>x</Popover.Close>
        </Popover.Header>
        <Popover.Description>Click x to dismiss</Popover.Description>
      </Popover.Content>
    </Popover>
  );
}

render(<CloseDemo />);

Controlled


function ControlledDemo() {
  const [open, setOpen] = React.useState(false);

  return (
    <div className="flex flex-wrap gap-3">
      <Popover open={open} onOpenChange={setOpen}>
        <Popover.Trigger as={Button}>Controlled</Popover.Trigger>
        <Popover.Content>
          <Popover.Description>Controlled popover</Popover.Description>
        </Popover.Content>
      </Popover>
      <Button onClick={() => setOpen(!open)}>{open ? 'Hide' : 'Show'}</Button>
    </div>
  );
}

render(<ControlledDemo />);

Accessibility & Keyboard

  • Trigger uses aria-expanded and aria-controls pointing to the content.
  • Content has role="dialog" (or role="popover").
  • Focus is trapped inside the content when modal is true.
  • Escape key dismisses the popover and returns focus to the trigger.
  • Click outside the popover dismisses it (non-modal mode).
KeyBehavior
EscapeDismiss the popover.
TabNavigate focusable content.
EnterActivate trigger / close button.

API Reference

Popover

See Spar Popover docs for primitive behavior.

Props

NameTypeDefaultDescription
childrenReact.React.ReactNode-PopoverTrigger and PopoverContent components
idstring-Custom base ID for ARIA relationships. If not provided, one will be generated automatically. Sub-element IDs are derived as ${id}-trigger and ${id}-content.
disabledbooleanfalseDisables all popover triggers (prevents opening)
openboolean-Controlled state for popover visibility
defaultOpenbooleanfalseInitial open state for uncontrolled mode
modalbooleanfalseWhether popover should behave modally (focus trap + backdrop)

Events

NameTypeDefaultDescription
onOpenChange(open: boolean) => void-Callback when popover open state changes

Popover.Content

Props

NameTypeDefaultDescription
childrenReact.React.ReactNode-
variantPopoverVariant'white'Color variant.
classNamesPartial<Record<"root", string>>-Per-slot extra classes.
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-Per-slot HTML-attribute overrides.
containerHTMLElement | nulldocument.bodyPortal container element. Content is portaled to document.body by default.
trapFocusbooleanfalseWhether to trap focus within content
sideSide'bottom'Side of trigger to position against
alignAlign'center'Alignment relative to trigger
classNamestring-

Events

NameTypeDefaultDescription
onOpenAutoFocus(event: Event) => void-Called when popover opens and focus moves inside
onCloseAutoFocus(event: Event) => void-Called when popover closes and focus returns to trigger
onEscapeKeyDown(event: KeyboardEvent) => void-Called when escape is pressed
onPointerDownOutside(event: PointerEvent) => void-Called when pointer down occurs outside the content
onInteractOutside(event: PointerEvent | FocusEvent) => void-Called when interaction occurs outside the content
onFocusOutside(event: FocusEvent) => void-Called when focus moves outside the content

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-variantAlwaysReflects the resolved variant for theme recipe scoping.

Popover.Trigger

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Popover.Arrow

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for the arrow.

Popover.Close

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for the close button.

Type Definitions

NameDefinition
PopoverVariant'white' | 'dark' | 'info' | 'success' | 'warning' | 'danger' | 'neutral'
Side'top' | 'right' | 'bottom' | 'left'
Align'start' | 'center' | 'end'
PopoverTriggerRenderProps{ isOpen: boolean; disabled: boolean; open: () => void; close: () => void; toggle: () => void }
PopoverCloseRenderProps{ isOpen: boolean; close: () => void }