Skip to content

BbPopover

A floating panel anchored to a trigger, for content the reader moves into and acts on.

import { BbPopover } from 'bitboss-ui';

On this page

Use it for

Reach for BbPopover when a trigger should open a small panel the user then works in: quick settings, a filter, a notifications list, an inline detail card. Fields, switches, links and buttons all belong inside it.

Use something else when

  • BbTooltip: the reader only needs a sentence about the control in front of them
  • BbHoverCard: the panel previews what a link points at and should open on hover, not on click
  • BbDropdown: the panel is only a list of actions
  • BbDialog: the task owns the page until it is resolved
  • BbOffCanvas: it is supplementary side content, docked to an edge rather than to a control

Passthrough

Hover or tap a part to outline it. The toggles turn on loading, errors and warnings where the component has them; only parts get a row.

Edit in place

Put your trigger in the activator slot and spread its props onto it.

That one spread is the whole contract: it carries the anchor, the element id, the ARIA attributes and the open handler. Miss it and you get a button that looks right and does nothing.

Q3 revenue review

width takes pixels, or a percentage of the trigger's width. Only the percentage form re-measures when the trigger resizes; leave width off and the panel sizes to its content.

Closing the panel

The floating panel renders no close button of its own. The affordance is yours, and the default slot hands you close.

Call close after the write, not before. A handler that dismisses first and commits second is one early return away from losing the edit silently.

Escape and a click outside dismiss the panel too, and for a read-only panel that is enough. A panel holding a form needs a visible way out as well, Cancel beside Save, because nothing on screen says Escape is safe. The header and footer slots receive close on the same scope.

Coming from v2

show-close and close-label are gone.

Focus and the page behind

Opening moves focus to the first focusable element in the panel; closing returns it to the trigger.

25 of 25

When that first element is not the one the reader came for, point focus-target at the right one with a CSS selector.

The page behind stays live. A click outside closes the panel and still reaches what was clicked (focus stays there), and Tab past the last control leaves the panel and closes it.

Set modal when the reader must finish or dismiss the panel first: the page goes inert, Tab stays inside, and an outside click only closes it. The phone sheet is modal either way.

Never open the popover from a @focus handler on the trigger: closing hands focus back to it, which opens the panel again.

The panel paints in the top layer, so it is never clipped by an overflow: hidden ancestor and never needs a z-index.

Coming from v2

The restore-focus prop is gone. Restoring focus is behaviour now, not a knob.

Placement and geometry

placement puts the panel on a side of the trigger: the top, right, bottom and left families, each with -start and -end alignment.

It defaults to bottom, and it flips itself near a viewport edge. Pick what reads best in the common case rather than guarding against the exception.

Three props tune the rest:

  • offset: the gap from the trigger (4)
  • padding: the gap the panel keeps from the page edge (10)
  • boundary: an element, ref or selector the panel stays inside alongside its trigger, usually a sidebar or a table. Away from the trigger it may pass that edge; only the viewport flips it

The panel stays tethered to its trigger: scroll the trigger out of view and the popover closes.

Coming from v2

A popover no longer draws an arrow, so hide-arrow and arrow-padding are gone. If you passed :hide-arrow="false" in v2 to get a bubble with a point on it, that shape is a BbTooltip now.

Header, footer and long content

scrollable caps the panel at the height still available and scrolls the body inside it.

The header and footer slots sit outside that scroll region, each with its own padding and divider. A title stays put and footer actions stay reachable without a sticky workaround.

Use it for feeds and long lists. Leave it off for atomic content: a calendar that scrolls internally is a worse calendar.

Triggers and cursor anchoring

trigger chooses the gesture: click by default, contextMenu for right-click and long-press, or an array to accept both.

Designing for keyboard-only users

Focus order, visible rings and skip links: the three checks we now run on every release before it ships.

accessibility

Right-click or long-press the article to tag it.

Pair contextMenu with placement="cursor" and the panel anchors where the pointer was rather than on the element.

Keep the distinction: a plain list of actions is BbDropdown. A context popover earns its place when the surface holds controls the user operates in place.

Driving it from outside

v-model opens and closes the popover: write true to open and false to close. Any control can write it, like the Change link below, and the popover writes the model back on every dismiss, so reading it stays accurate.

Weekly digest is on.

activator is for a trigger that already exists somewhere else, like a toolbar button or a table cell. Pass the element or the component ref and the popover attaches the listeners and the ARIA itself. There is no props to spread, and the activator slot is not rendered.

The body is lazy: it renders on the first open. Set eager to render it at mount, for a chart that must measure itself or a test that queries a closed panel.

@shown fires once the open transition has run and focus is inside the panel, and @hidden once the panel has closed. Both wait for the transition on the floating panel and on the sheet, so they are the safe place to measure or to move focus yourself.

Coming from v2

popoverRef.value.show(), .close() and .isOpen are gone, replaced by v-model. In an untyped template the leftover call is undefined at runtime rather than a compile error, which makes it the quietest break on this component.

One trigger per row

A rename button on every row means two component instances per row before anyone opens anything. The v-bb-popover directive costs what a plain button costs: the trigger carries aria-haspopup="dialog" and nothing else until the click that opens it. From then on it is the full popover, focus handling included.

  • Q3 revenue
  • Churn by plan
  • Support backlog

A directive has no slot, so the body is a portal. Declare a BbPopoverPortal once and open it by name, with the name in inner quotes (v-bb-popover="'rename-report'"); a bare rename-report is a subtraction and opens nothing. The portal's slot gets { data, close }, where data is the binding's data. The argument is the placement (v-bb-popover:bottom-end), and the object form takes any BbPopover prop beside portal and data.

Passing a component instead (v-bb-popover="RenameForm") renders it with the keys of data as its props, but it has no way to close the popover. Content that needs close is a named portal or a BbPopover in the template.

The mobile sheet

Below 768px a popover opens as a bottom-anchored, draggable BbOffCanvas sheet instead of a floating panel.

That is the shipped behaviour, and the header, default and footer slots carry over unchanged. Pass :adaptive="false" to keep the floating panel at every width.

The sheet shows a close button × that the floating panel does not have, so a popover holding a form always has a way out on a phone. When your own footer already closes it, hide the × with :off-canvas-props="{ hideClose: true }", as the rename and settings demos above do.

width is ignored in sheet mode. A sheet sizes itself. Tune it with off-canvas-props, which merges over the defaults side: 'bottom', size: 'auto', draggable: true.

The surface is decided when the panel opens and frozen until it closes, so resizing the window never swaps a live panel into a sheet.

Styling the floating and sheet surfaces

The panel takes its paint from the theme tokens and gives you four class hooks: .bb-popover__panel, the bordered box, and __header, __content and __footer inside it. Every BbPopover in the project carries them, so put your own class on the popover and scope the rules to it.

vue
<BbPopover class="team-popover">…</BbPopover>
css
.team-popover .bb-popover__header {
    padding: 12px;
    font-weight: 600;
}

.team-popover .bb-popover__content {
    padding: 12px;
}

.team-popover .bb-popover__footer {
    background-color: var(--bb-muted);
}

Those hooks and your class live on the floating panel only. Below 768px the popover is a BbOffCanvas sheet that neither reaches. A class on the popover lands on an invisible positioning wrapper the sheet does not have, though a data-* attribute still gets there. When a look has to hold on a phone, set it through pt. pt:panel is the bordered box on desktop and the sheet itself on a phone, and pt:header, pt:content and pt:footer follow the bands into the sheet.

Keep the look off pt:root. Padding, a border or a background there does not style the box. It draws a frame around it, and a click on that frame counts as outside and closes the popover. Desktop geometry on pt:panel (rounded-md, w-64, px-1) reshapes the sheet too: correct it for the phone with pt:sheet, which wins there. And keep overflow-hidden off a panel that can become a sheet, or a gap shows under it while it is dragged.

Coming from v2

theme is gone. Colour comes from --bb-panel, --bb-border, --bb-radius and --bb-shadow.