Popover
A floating panel that appears next to a trigger element. Useful for tooltips with rich content, contextual menus, additional details, or any supplementary information that should appear on demand.
Example
A simple popover with a title and description.
<div buiPopover>
<button matButton buiPopoverTrigger>Open Popover</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>
<h3 buiPopoverTitle>Dimensions</h3>
<p buiPopoverDescription>
Set the dimensions for the layer. Width and height
can be adjusted independently.
</p>
</div>
</ng-template>
</div>Directives
The directives are standalone. Import only the slots the popover actually uses. Trigger, close, portal, content, title and description require the root.
import {
BuiPopover,
BuiPopoverAnchor,
BuiPopoverClose,
BuiPopoverContent,
BuiPopoverDescription,
BuiPopoverPortal,
BuiPopoverTitle,
BuiPopoverTrigger,
} from '@/bui/ui/popover';| Directive | Description |
|---|---|
buiPopover | Root container. Manages open state, anchor positioning, and the CDK overlay. |
buiPopoverTrigger | Opens the popover when clicked. Also serves as the default positioning anchor unless an external anchor is provided. |
buiPopoverAnchor | Marks an element as the positioning anchor for the popover, allowing the overlay to be positioned relative to a different element than the trigger. Optional. |
buiPopoverPortal | Wraps the content that is rendered inside a CDK overlay when the popover opens. |
buiPopoverContent | The floating panel itself. Handles entry/exit animations. |
buiPopoverClose | Closes the popover on click or Enter. |
buiPopoverTitle | Title text inside the content. Connects to aria-labelledby. Optional. |
buiPopoverDescription | Description text inside the content. Connects to aria-describedby. Optional. |
This is how the slots nest.
<div buiPopover>
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>
<h3 buiPopoverTitle>Title</h3>
<p buiPopoverDescription>Description</p>
<button matButton buiPopoverClose>Close</button>
</div>
</ng-template>
</div>
<!-- With an external anchor -->
<span buiPopoverAnchor #myAnchor="buiPopoverAnchor">
Anchor
</span>
<div buiPopover [anchor]="myAnchor">
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>
<h3 buiPopoverTitle>Title</h3>
<p buiPopoverDescription>Description</p>
<button matButton buiPopoverClose>Close</button>
</div>
</ng-template>
</div>Popover
The root directive. Apply it to any element to create a popover instance. It manages the open state, positions the overlay relative to the trigger, and handles dismissal via backdrop click or Escape key.
<!-- Default (bottom) -->
<div buiPopover>...</div>
<!-- Custom position -->
<div buiPopover position="top-start">...</div>
<!-- Custom offset -->
<div buiPopover [offset]="8">...</div>
<!-- Two-way open binding -->
<div buiPopover [(open)]="isOpen">...</div>
<!-- Close on scroll -->
<div buiPopover [closeOnScroll]="true">...</div>
<!-- No backdrop -->
<div buiPopover [hasBackdrop]="false">...</div>
<!-- External anchor -->
<div buiPopover [anchor]="myAnchor">...</div>| Prop | Type | Default | Description |
|---|---|---|---|
open | model<boolean> | false | Two-way binding for the open state. |
anchor | BuiPopoverAnchor | ElementRef | Element | { x: number; y: number } | null | null | An external anchor to position the popover against. When set, the overlay is positioned relative to this anchor instead of the trigger. Accepts a buiPopoverAnchor directive reference, an ElementRef, a native DOM element, or a fixed point ({ x, y }). |
position | PopoverPosition | 'bottom' | Preferred placement relative to the trigger. Automatically flips to a fallback position if there is not enough space. |
offset | number | 4 | Distance in pixels between the trigger and the popover panel. |
autoFocus | 'dialog' | 'first-tabbable' | 'first-heading' | string | undefined | 'first-tabbable' | Configures where focus goes when the dialog opens. |
closeOnScroll | boolean | false | When true, the popover automatically closes when the user scrolls any scrollable ancestor. |
hasBackdrop | boolean | true | When false, no backdrop is rendered and outside clicks pass through to the page while still closing the popover. |
restoreFocus | boolean | string | HTMLElement | true | Configures where focus returns when the dialog closes. |
(opened) | void | — | Emits when the popover opens. |
(closed) | void | — | Emits after the popover closes and its exit animation completes. |
The position input accepts one of 12 placement values:
top,top-start,top-endbottom,bottom-start,bottom-endstart,start-top,start-bottomend,end-top,end-bottom
Trigger
Apply buiPopoverTrigger to a button or any clickable element. It toggles the popover on click, automatically sets aria-expanded, aria-controls, and keyboard support (Enter / Space). The trigger element also serves as the default positioning anchor for the overlay unless an external anchor is provided.
<button matButton buiPopoverTrigger>Open Popover</button>Anchor
By default the popover is positioned relative to the trigger element. When you need the overlay to appear next to a different element, use the anchor input on buiPopover.
The simplest approach is the buiPopoverAnchor directive. Apply it to any element and pass the reference to the anchor input:
<span buiPopoverAnchor #myAnchor="buiPopoverAnchor">
Anchor element
</span>
<div buiPopover [anchor]="myAnchor">
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>...</div>
</ng-template>
</div> You can also pass an ElementRef, a native DOM Element, or a fixed point ({ x: number; y: number }) for absolute positioning:
<!-- Fixed point -->
<div buiPopover [anchor]="{ x: 200, y: 400 }">
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>...</div>
</ng-template>
</div><div buiPopover [anchor]="externalAnchor">
<button matButton buiPopoverTrigger>Open Popover</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>
<p buiPopoverDescription>
This popover is anchored to the pill
on the right, not the trigger button.
</p>
</div>
</ng-template>
</div>
<span
buiPopoverAnchor
#externalAnchor="buiPopoverAnchor"
class="rounded-full bg-neutral-a3 px-3 py-1 text-xs font-medium"
>
Anchor element
</span>Following the anchor NEW
While the popover is open, it watches its anchor with a ResizeObserver and repositions whenever the anchor changes size. A trigger whose label grows or shrinks keeps the panel attached. Watching stops as soon as the popover starts closing, so the panel holds still through its exit animation, and ends when the popover is destroyed.
Only the anchor element itself is observed. A fixed point ({ x, y }) has no element to watch, and an anchor that moves because of layout changes around it, without resizing, is not followed.
<div buiPopover>
<button matButton buiPopoverTrigger>
{{ long() ? 'Anchor with a much longer label' : 'Anchor' }}
</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>
<p buiPopoverDescription>
Resize the trigger and this popover follows it.
</p>
<div class="mt-3 flex justify-end">
<button matButton (click)="long.set(!long())">
Resize
</button>
</div>
</div>
</ng-template>
</div>Matching the anchor width NEW
The popover exposes the width of its anchor as --popover-anchor-width and keeps it current while the anchor resizes. Add w-(--popover-anchor-width) to buiPopover to match the trigger, or min-w-(--popover-anchor-width) to be at least as wide as it. A fixed point has no width, so the variable is not set.
<div buiPopover class="w-(--popover-anchor-width)">
<button matButton buiPopoverTrigger class="w-full max-w-64 spread">
<mat-icon svgIcon="chevron-down" iconPositionEnd/>
Open Popover
</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>
<p buiPopoverDescription>
This popover is as wide as its trigger.
</p>
</div>
</ng-template>
</div>Portal
The buiPopoverPortal directive goes on an ng-template. Its content is projected into a CDK overlay anchored to the trigger when the popover opens.
<ng-template buiPopoverPortal>
<div buiPopoverContent>
<!-- title, description, or any custom content -->
</div>
</ng-template>Content
The buiPopoverContent directive marks the floating panel. It handles entry/exit animations and provides the visual styling (border, shadow, background). Place title, description, or any custom content inside it. The content radius matches the menu radius by default and can be changed through --popover-border-radius on buiPopoverContent, for example [--popover-border-radius:calc(var(--theme-border-radius)*1.5)].
<div buiPopoverContent>
<h3 buiPopoverTitle>Title</h3>
<p buiPopoverDescription>Description text</p>
</div>
<!-- Custom content -->
<div buiPopoverContent>
<p>Any content can go here.</p>
<button matButton buiPopoverClose>Close</button>
</div>Close
Apply buiPopoverClose to any element inside the portal. It closes the popover on click or Enter. Attach your own (click) handler separately for additional logic.
<button matButton buiPopoverClose (click)="onSave()">
Save & Close
</button>Title & Description
buiPopoverTitle and buiPopoverDescription are optional directives for structured content. The title connects to aria-labelledby and the description connects to aria-describedby on the popover container.
<div buiPopoverContent>
<h3 buiPopoverTitle>Dimensions</h3>
<p buiPopoverDescription>
Set the dimensions for the layer.
</p>
</div>Position
Use the position input to control where the popover appears relative to the trigger. The popover automatically flips to a fallback position if there is not enough space.
<!-- Bottom (default) -->
<div buiPopover position="bottom">...</div>
<!-- Top -->
<div buiPopover position="top">...</div>
<!-- Start (left in LTR) -->
<div buiPopover position="start">...</div>
<!-- End (right in LTR) -->
<div buiPopover position="end">...</div>
<!-- Aligned variants -->
<div buiPopover position="bottom-start">...</div>
<div buiPopover position="top-end">...</div>Backdrop NEW
By default the popover renders a transparent backdrop. It catches the next click anywhere outside the panel, closing the popover and swallowing the click. Set hasBackdrop to false to skip it: the popover still closes on outside clicks, but the click reaches the element underneath and acts on it. Clicks on the trigger are ignored by that path, so the trigger keeps toggling as usual. Escape and buiPopoverClose behave the same either way.
<!-- With backdrop (default) -->
<div buiPopover>
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>...</div>
</ng-template>
</div>
<!-- Without backdrop -->
<div buiPopover [hasBackdrop]="false">
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>...</div>
</ng-template>
</div>Programmatic control
You can control the popover without a trigger. Use the open model for two-way binding, or grab a template reference via #ref="buiPopover" and call open(), close(), or toggle().
<!-- Two-way binding -->
<div buiPopover [(open)]="isPopoverOpen">
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>...</div>
</ng-template>
</div>
<!-- Template reference -->
<div buiPopover #myPopover="buiPopover">
<button matButton buiPopoverTrigger>Open</button>
<ng-template buiPopoverPortal>
<div buiPopoverContent>...</div>
</ng-template>
</div>
<button matButton (click)="myPopover.open()">Open</button>
<button matButton (click)="myPopover.close()">Close</button>
<button matButton (click)="myPopover.toggle()">Toggle</button>Focus and accessibility
The panel opens through a CDK dialog. It renders inside a role="dialog" container, focus moves into the panel on open and a focus trap keeps it there while the popover is open. The autoFocus input picks the target, the first tabbable element by default, and restoreFocus controls where focus lands on close, by default back on the trigger.
The trigger carries aria-haspopup="dialog", aria-expanded, and aria-controls pointing at the panel, while buiPopoverTitle and buiPopoverDescription feed aria-labelledby and aria-describedby. Escape closes the popover and returns focus to the trigger.
Because focus moves into the panel, the popover fits content you work in: forms, lists, and menus. Patterns that need focus to stay on the trigger, such as a filtering input driving its list through aria-activedescendant, are not a fit.
See it in blocks
- Simple shows a title and description panel.
- Positions places one trigger per side to show the
positioninput. - With form holds a dimensions form inside the panel.
- Notifications lists filtered notifications under a bell trigger.
- Workspace switcher picks a workspace from a searchable list.
- Simple table reorders and hides columns from a Customize popover.