Skip to content

PlPopover

A sheet that opens beside the thing that opened it. Unlike a tooltip it can be reached; unlike a modal it does not take the page.

React
tsx
import { PlButton, PlPopover } from 'plass-ui';

<PlPopover trigger={<PlButton>How is this worked out?</PlButton>} title="Effective rate">
  Your rate is the base rate plus whatever your plan adds to it.
</PlPopover>;
dart
import 'package:plass_ui/plass_ui.dart';

PlPopover(
  open: explaining,
  onOpenChanged: (bool next) => setState(() => explaining = next),
  title: const Text('Effective rate'),
  trigger: PlButton(
    onPressed: () => setState(() => explaining = true),
    child: const Text('How is this worked out?'),
  ),
  child: const Text('The base rate plus whatever your plan adds to it.'),
);

Props

PropTypeDefaultDescription
triggerReactElementThe element the popup hangs off and that opens it
titleReactNodeThe heading, and what names the popup
descriptionReactNodeA line under the title, and the popup's accessible description
sideshared'top' | 'right' | 'bottom' | 'left''bottom'Which edge of the trigger it appears on. Flips to the opposite side when there is no room
alignshared'start' | 'center' | 'end''center'Where it sits along that edge
sideOffsetnumber6Distance from the trigger, in pixels
alignOffsetnumber0Shift along that edge, in pixels
arrowbooleanfalseThe little wedge pointing at the trigger. Off by default: a wedge past the box cannot carry the blurred backdrop
openbooleanWhether the popover is open. Use with onOpenChange for a controlled one
defaultOpenbooleanWhether it starts open, for an uncontrolled one
onOpenChange(open: boolean) => voidCalled when the open state changes
modalboolean | 'trap-focus'falseWhether the page behind is taken away. false is what separates a popover from a modal
dismissiblebooleantrueWhether pressing Escape or clicking outside closes the popup
showClosebooleanfalseShows the × in the corner
closeLabelstring'Close'Accessible name of the × button
widthnumber | stringA hard cap on the popup's width, overriding the one size implies
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The radius, the padding and how wide the popup is allowed to get
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. It reaches the focus rings inside and nothing else
densityshared'default' | 'compact''default'The popup's inner padding
PropTypeDefaultDescription
open * boolWhether the popover is open. Use with onOpenChange for a controlled one
trigger * WidgetThe element the popup hangs off. Required here: a LayerLink has nothing to follow without one
onOpenChangedValueChanged<bool>?Called with what the open state should become
childWidget?The body
titleWidget?The heading, and what names the popup
descriptionWidget?A line under the title, and the popup's accessible description
sidesharedPlassSidePlassSide.bottomWhich edge of the trigger it appears on. Flips to the opposite side when there is no room
alignsharedPlassAlignPlassAlign.centerWhere it sits along that edge
offsetdouble6How far it stands off the trigger, in logical pixels
arrowboolfalseThe little wedge pointing at the trigger. Off by default: a wedge past the box cannot carry the blurred backdrop
dismissiblebooltrueWhether a press outside closes the popup
showCloseboolfalseShows the × in the corner
closeLabelString'Close'Accessible name of the × button
widthdouble?A hard cap on the popup's width, overriding the one size implies
sizesharedPlassSizePlassSize.mdThe radius, the padding and how wide the popup is allowed to get
colorsharedPlassColorPlassColor.primarySemantic colour role. It reaches the focus rings inside and nothing else
densitysharedPlassDensityPlassDensity.standardThe popup's inner padding

Every other <div> attribute passes through to the popup.

What the shared axes mean across the library is in prop conventions.

Popover, tooltip or modal

Three floating surfaces, three different jobs, and the difference between them is what you can do with them.

  • A PlTooltip is a note about something else. It appears on hover, it goes away when you leave, and nothing in it can be reached. A link inside one is a link nobody can click.
  • A popover stays up until it is dismissed. It can be entered with the pointer or the keyboard, and what is inside it can be clicked and typed into.
  • A PlModal takes the page away until it is answered.

A popover is the middle one: anchored to a control, and the page behind goes on working. That is what modal defaults to false says.

No variant and no elevation

The three materials answer "how much does this surface assert itself against the page", and a popup that had to be asked for has already answered it. And a popover genuinely floats, which is the one case the elevation ladder exists for, so it is fixed at its top rung rather than offered as a decision that could sit it flat.

Examples

side and align

Which edge of the trigger it appears on, and where it sits along that edge. It flips to the opposite side when there is no room, and never slides along the edge it is on, which is what keeps an arrow pointing at the thing it belongs to.

React

arrow

Off by default, unlike on a PlTooltip. A tooltip is a filled plate and its wedge is the same solid colour; this surface is translucent over a blurred backdrop, and a wedge sticking out past the popup's own box cannot carry that backdrop with it.

Turn it on where the trigger is far enough away that the popup needs to say what it belongs to.

A popover can hold a form

This is the whole reason it is not a tooltip. What is inside can take focus, so a rename, a filter or a date range belongs here rather than in a modal that would have taken the page away to ask one question.

React

dismissible

On by default: a press outside closes it. Turn it off only for a popup that has its own way out, and then give it one (a close button, an action that answers it), because there will be no other.

Escape closes it too, and dismissible={false} cancels both. PlPopoverClose still works while it is off, which is what keeps a refusal from being a trap.

showClose and the actions inside it still work while it is off, which is what keeps a refusal from being a trap.

Accessibility

  • title names the popup and is announced as a heading; description sits under it.
  • The screen behind is not taken away. A popover that hid the page would be a modal with a worse shape.
  • A press outside is a real dismissal, and it can be refused with dismissible.
  • The popup is a dialog anchored to its trigger, and focus goes back to the trigger on the way out. Base UI owns the anchoring, the flip at the window edge, the outside-press and Escape handling, the focus return and the aria-labelledby / aria-describedby wiring.
  • modal="trap-focus" holds focus inside without locking the page's scroll.
  • The lift, the anchoring, the flip and the press outside are PlassAnchoredPortal's, the same layer a PlTooltip and a PlSelect's list stand on, so the three stay stuck to their anchors through a scroll for the same reason.

Differences from the React build

ReactFlutterWhy
open / defaultOpen / onOpenChangeopen / onOpenChangedFlutter's own controls are controlled, and so is every stateful widget in this package.
trigger is optionaltrigger is requiredA browser can position a popup against the viewport with no anchor; a LayerLink has nothing to follow without one.
modalThere is no page scroll to lock and no inert tree to build. The press outside is the whole of what a popover needs to answer.
alignOffsetThe anchoring is a flip and never a slide, so there is no along-the-edge shift to offset.
PlPopoverCloseIt exists over there so an uncontrolled popover's button has something to call. Every popover here is controlled.
width: number | stringwidth: doublePixels stay pixels. There is no CSS length to accept.
className, styleThere is no class list and no style attribute to pass through.

Released under the MIT License