Popover

The Mosaic Popover — the styled Mosaic component composed from the @clerk/headless popover primitive and themed with StyleX. It owns only what it means to float: trigger wiring, ARIA, focus management, positioning, stacking, viewport clamps, and the enter/exit transition. It paints no surface of its own — background, border, radius, shadow and padding come from whatever you render inside it, usually a Card. Keeping the two apart means only one element ever draws the border.

Example

Usage

Compose the parts: Popover.Root owns the open state and placement, Popover.Trigger is the anchor, and Popover.Popup is the floating box. Put the surface inside the popup — everything you see comes from the Card.

import { Button } from '@clerk/ui/mosaic/components/button';
import { Card } from '@clerk/ui/mosaic/components/card';
import { Popover } from '@clerk/ui/mosaic/components/popover';

<Popover.Root>
  <Popover.Trigger render={props => <Button {...props}>Open popover</Button>} />
  <Popover.Popup aria-label='Account'>
    <Card.Root>
      <Card.Content>Flexible inner content.</Card.Content>
      <Card.Footer>
        <Button
          color='negative'
          fullWidth
        >
          Sign out of all accounts
        </Button>
      </Card.Footer>
    </Card.Root>
  </Popover.Popup>
</Popover.Root>;

Popover.Trigger renders a <button> by default. Its render prop swaps in your own element and receives the interaction props (ARIA attributes, click handler, and the data-open / data-closed state attributes) from the headless layer to spread onto it.

Popover.Popup portals itself out to the document body and positions against the trigger, so it needs no wrapper of its own.

Accessible name

The popup is a role="dialog", so it needs a name. Either pass aria-label to Popover.Popup, or render a Popover.Title inside it — the title wires aria-labelledby for you. A popover with neither logs a development warning.

<Popover.Root>
  <Popover.Trigger>Open</Popover.Trigger>
  <Popover.Popup aria-label='Account'>
    <Card.Root>
      <Card.Content>Flexible inner content.</Card.Content>
    </Card.Root>
  </Popover.Popup>
</Popover.Root>;

Controlled

const [open, setOpen] = useState(false);

<Popover.Root
  open={open}
  onOpenChange={setOpen}
>
  <Popover.Trigger>Open</Popover.Trigger>
  <Popover.Popup aria-label='Account'>
    <Card.Root>
      <Card.Content>Flexible inner content.</Card.Content>
    </Card.Root>
  </Popover.Popup>
</Popover.Root>;

Size

size on Popover.Popup sets the width of the floating box: sm (18rem), md (23.5rem, the default), or lg (26rem). Each clamps to calc(100vw - 2rem) on narrow viewports. The popup also caps its height at min(80dvh, 36rem) — for taller content, give the surface inside a scrolling region.

<Popover.Root>
  <Popover.Trigger>Open</Popover.Trigger>
  <Popover.Popup size='lg'>
    <Card.Root>
      <Card.Content>A wider panel.</Card.Content>
    </Card.Root>
  </Popover.Popup>
</Popover.Root>;

Placement

placement on Popover.Root sets the preferred side: top, right, bottom (the default), or left. sideOffset sets the gap from the trigger.

Placement is a preference, not a guarantee — the popup flips to the opposite side when it would overflow, and shifts along the cross axis to stay in view. data-side on the positioner always reports the side actually used.

Alignment

Append -start or -end to a placement to align the popup with that edge of the trigger instead of centering on it.

<Popover.Root
  placement='bottom-end'
  sideOffset={8}
>
  <Popover.Trigger>Open</Popover.Trigger>
  <Popover.Popup aria-label='Account'>
    <Card.Root>
      <Card.Content>Aligned to the trigger's end edge.</Card.Content>
    </Card.Root>
  </Popover.Popup>
</Popover.Root>;

Cross-axis flipping is only enabled for aligned placements, so bottom-start may become bottom-end near a viewport edge while a plain bottom will not.

Parts

PartSlotDescription
Popover.RootState provider; owns open/close, placement, sideOffset, modal.
Popover.TriggerAnchor element; renders a <button> and accepts a render prop.
Popover.Popuppopover-popupThe floating box; owns size, viewport clamps, and the enter/exit run.
Popover.CloseDismisses the popover; accepts a render prop.
Popover.TitleHeading; wired to the popup's aria-labelledby.
Popover.DescriptionDescription; wired to the popup's aria-describedby.

Popover.Popup renders the portal and the floating positioner internally. The positioner carries the popover-positioner slot and is the role="dialog" element, which is why the popup's aria-label / aria-labelledby land there.

Popover.Title and Popover.Description are unstyled passthroughs from the headless layer — render them through your own typography (Heading, Text) inside the surface.

Styling

Unlike the slot-recipe components, the Mosaic popover is themed with StyleX. Each styled part carries a stable .cl-<slot> class (the slots in the table above) alongside the StyleX atoms. Consumers never target the hashed atomic classes — override by targeting the .cl-* slot from a CSS layer that wins over @clerk/ui/styles.css:

@import '@clerk/ui/styles.css' layer(components);

@layer overrides {
  .cl-popover-popup[data-size='lg'] {
    width: 30rem;
  }
}

The popup is intentionally transparent. To restyle the panel's background, border, radius or shadow, style the surface you render inside it (e.g. .cl-card-root) rather than the popup.

State attributes from the headless layer are available for CSS targeting:

AttributeApplies ToDescription
data-openTrigger, PopupPresent when the popover is open
data-closedTrigger, PopupPresent when closed (during exit)
data-starting-stylePopupPresent on the entering frame
data-ending-stylePopupPresent during the exit animation
data-sidePositionerResolved side (top / bottom / left / right)
data-sizePopupResolved size (sm / md / lg)

The popup's enter/exit transition (opacity + scale) is driven off data-starting-style / data-ending-style, runs for --cl-duration-base, and eases per property: linear for the fade, --cl-ease-default for the scale. Under prefers-reduced-motion: reduce only transform drops out of the transition — the fade still runs, since the vestibular concern is the movement.

data-open on the trigger is what a trigger component styles to hold a pressed/active look while the popover is open.