Popover

An overlay positioned relative to its trigger, built on React Aria. Placement is collision-aware, so a popover near the viewport edge flips rather than clipping. While it's open, focus is contained inside; Esc and a press outside close it.

import { Popover } from "@pockets-ui/components"
import { button } from "@/styled-system/recipes"

Reach for a popover when the content is interactive or worth dwelling on. For a short, non-interactive description on hover or focus, use Tooltip instead.

Anatomy

PartRole
Popover.RootWires the trigger to the overlay and owns open state.
Popover.TriggerThe focusable element that opens the popover.
Popover.ContentThe positioned surface — focus container and your content.

Popover.Trigger renders its own element rather than wrapping a Button, so these examples style it with the button recipe — button({ variant: "solid" }) gives the same classes the component uses.

Basic

Popover.Content wraps a dialog, which needs an accessible name. Give it aria-label when the content has no visible heading to point at.

Code
import { Popover } from "@pockets-ui/components"
import { css } from "@/styled-system/css"
import { button } from "@/styled-system/recipes"

// Popover.Trigger renders its own element, so style it with the button recipe
// rather than nesting a <Button> inside.
const trigger = button({ variant: "solid" })

const heading = css({ fontWeight: "semibold", color: "fg" })
const body = css({ fontSize: "sm", color: "fg.subtle", lineHeight: "relaxed" })

export default function PopoverBasic() {
  return (
    <Popover.Root>
      <Popover.Trigger className={trigger}>Show details</Popover.Trigger>
      <Popover.Content aria-label="Order details">
        <p className={heading}>Order #1024</p>
        <p className={body}>
          Shipped 2 days ago via ground. Estimated delivery is tomorrow by 8pm.
        </p>
      </Popover.Content>
    </Popover.Root>
  )
}

Placement

placement is the preferred side and defaults to "bottom start"; React Aria moves the popover if it won't fit there. offset controls the gap from the trigger and defaults to 8.

Code
import { Popover } from "@pockets-ui/components"
import { css } from "@/styled-system/css"
import { button } from "@/styled-system/recipes"

const trigger = button({ variant: "solid" })

const body = css({ fontSize: "sm", color: "fg.subtle", lineHeight: "relaxed" })

export default function PopoverPlacement() {
  return (
    <>
      <Popover.Root>
        <Popover.Trigger className={trigger}>Top</Popover.Trigger>
        <Popover.Content placement="top" aria-label="Shown above">
          <p className={body}>Shown above the trigger.</p>
        </Popover.Content>
      </Popover.Root>
      <Popover.Root>
        <Popover.Trigger className={trigger}>Bottom</Popover.Trigger>
        <Popover.Content placement="bottom" aria-label="Shown below">
          <p className={body}>Shown below the trigger.</p>
        </Popover.Content>
      </Popover.Root>
      <Popover.Root>
        <Popover.Trigger className={trigger}>Left</Popover.Trigger>
        <Popover.Content placement="left" aria-label="Shown to the left">
          <p className={body}>Shown to the left.</p>
        </Popover.Content>
      </Popover.Root>
      <Popover.Root>
        <Popover.Trigger className={trigger}>Right</Popover.Trigger>
        <Popover.Content placement="right" aria-label="Shown to the right">
          <p className={body}>Shown to the right.</p>
        </Popover.Content>
      </Popover.Root>
    </>
  )
}