ComponentsOverlays

Popover

A floating panel tied to a button. Use it for source details, pickers, filters, and short forms. Unlike a menu it can hold any content, and unlike a dialog it leaves the rest of the page usable.

default
menu
picker
panel
work-list
app-actions
bud

Open a size. Nothing here is saved.

Usage

Popover usageTSX
import { Button } from "@tulipfarm/sprout/button";
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@tulipfarm/sprout/popover";

<Popover>
  <PopoverTrigger render={<Button variant="outline" size="sm" />}>
    Sync status
  </PopoverTrigger>
  <PopoverContent align="end">
    <PopoverHeader>
      <PopoverTitle>Synced 2 minutes ago</PopoverTitle>
      <PopoverDescription>All 4 sources are up to date.</PopoverDescription>
    </PopoverHeader>
  </PopoverContent>
</Popover>

Pass a Button to PopoverTrigger through render. Icon-only triggers need an aria-label. Add a PopoverTitle or an aria-label on the content so the panel has a name.

Sizes

PopoverContent size picks the width and padding. "default" is an 18rem padded panel for text. "menu" (24rem) and "picker" (18rem) have no padding so search lists can run edge to edge. "panel" fits filters and tabs up to 32rem. "work-list" is as wide as a panel and caps its height so a header, scroll area, and footer fit. "app-actions" is a 20rem scrolling card, and "bud" is a fixed-height quick chat window.

Sources and help

Default-size popovers hold short explanations: a citation, why a repository is missing, or who can see a project. Load slow content in onOpenChange and show loading, error, and retry states inside the panel.

Sources and help

Default size: an 18rem padded panel for short text, centered under its trigger.

Chat citationsecondary icon-xs · default · center

Support backfills are approved

Source linkslink task-link · loads on open · ready, unavailable, error and retry
Integration helpghost xs · default · align end · external link
Project sharingscope badge · default · side top, align start · archived note and link

Status

A toolbar status button can open with defaultOpen to call out a problem. Keep a visually hidden role="status" or role="alert" next to it so changes are announced even when the panel is closed.

Toolbar status

A status button announces changes and opens details. Errors use the destructive button.

Savedoutline sm · default · align end
All changes to “Q4 hiring plan” were saved at 14:32.
Sync faileddestructive sm · role alert · action inside
Google Calendar didn’t accept 3 changes. Your edits are kept on this device.
Open on loaddefaultOpen · shows details once, then stays closed
Not mounted

Pickers

Pickers open above composers with side="top" and below toolbars by default. A picker trigger can also be a tooltip trigger. Show loading, failed, empty-search, and no-results states in the list, and clear the search when the picker closes.

Pickers

Menu (24rem) and picker (18rem) sizes have no padding, so a search list can run edge to edge. Composer pickers open above.

Chat composerghost sm · menu side top, align start · picker side top, align end · tooltip on trigger

Ask about your workspace…

Chat projectoutline default (below) · scope badge and ghost badge (above) · picker
Project list stateready, loading, failed with retry, no projects
Linked rowsoutline sm · picker · multi-select with aria-pressed

Panels

The panel size holds tabs and filter forms. Control it with open and onOpenChange to keep it open while a rule is invalid, and close it after a valid rule is added.

Table query bar

Panel size (32rem, never wider than the screen) holds forms and tabs. Search aligns to the end; filters align to the start.

Searchoutline sm, or secondary sm with the query · panel · align end · tabs
Filtersoutline sm (edit, truncated) and ghost sm (add) · panel · controlled · validation
Summariesoutline sm, or secondary sm when grouped · menu · header and description

Calendar

A date picker opens from the month title. The work-list size lists pending work and overlapping events, with a focusable scroll area and a footer link.

Calendar

Work lists cap their height at 36rem or the space left, and scroll only the list between a fixed header and footer.

Date titleghost calendar-title inside an h2 · default · align start · labelled content

Pending workoutline calendar with badge · work-list · align start · header, scroll region, footer
Overlapping eventscustom trigger class · work-list · align start · controlled

Hover cards

PopoverTrigger openOnHover opens after delay and closes after closeDelay. Pass initialFocus={(type) => type !== "mouse"} so pointer hover doesn’t steal focus, while keyboard and touch users land inside.

App actions

Hover a tile for 800ms, or click or press Enter. Keyboard opens move focus inside; mouse opens keep it on the tile.

One appBrandTile action + ghost icon · app-actions · align start, sideOffset 8
Several appsoverflow count · app names and icons · scrolls when tall
Disableddisabled tile never opens

Quick chat

A popover doesn’t need a trigger. Pass anchor to tie it to any element, keepMounted to keep its draft while closed, and modal={false} so the page stays usable. Cancel the outside press in onOpenChange when the launcher should toggle it, and return focus with finalFocus.

Anchored quick chat

No trigger: the bud size anchors to a launcher, stays mounted while closed, and doesn’t trap the page.

Launcher cornerside and align follow the corner
Errorsload failure and position failure, each with a retry

Placement

Set side, align, sideOffset, and alignOffset. Popovers open below and centered by default, and flip or shift to stay inside the viewport.

Placement

Pick a preferred side and alignment. The panel flips or shifts to stay on screen.

Alignmentstart, center (default), end

API

Popover and PopoverTrigger wrap the Base UI popover root and trigger and take their props, including modal, defaultOpen, openOnHover, delay, closeDelay, and render.

PopoverContent renders its own portal and positioner. It takessize, side ("bottom"), align ("center"), sideOffset (4), alignOffset, collisionPadding (8), anchor, keepMounted, container, and every Base UI popup prop such as initialFocus and finalFocus.

PopoverHeader is a div that stacks PopoverTitle (an h2) and PopoverDescription (a p). Every part sets a data-slot, and you can override it.

Accessibility

The trigger has aria-haspopup="dialog" and aria-expanded. Enter and Space open the popover and move focus inside. Tab moves through its content, and Escape or an outside click closes it and returns focus to the trigger.

Popovers fade and scale in from their side over 120ms; the app-actions and bud sizes take 180ms. Motion turns off when reduced motion is requested. In forced-colors mode the panel keeps a visible border.

Theme tokens

Popovers use --sprout-surface, --sprout-line, --sprout-text, --sprout-muted, --sprout-shadow-overlay, and the panel radius. They need no brand color, so they look the same with the neutral and TulipFarm themes in light and dark mode.