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.
Open a size. Nothing here is saved.
Usage
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.
Support backfills are approved
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.
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.
Ask about your workspace…
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.
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.
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.
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.
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.
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.