Skip to content

Popper ​

Headless floating UI primitive — a trigger and a teleported, positioned content panel. Aurora's Tooltip, Menu, and DatePicker are all built on it. Use it directly when you need a custom popover and the higher-level wrappers don't fit.

The three pieces:

  • Popper — root; manages open state and positioning via @popperjs/core
  • PopperTrigger — the element users click or hover to open
  • PopperContent — the floating body; teleports to <body> so it isn't trapped by overflow

Basic — click ​

html
<Popper>
  <PopperTrigger>
    <Button>Click me</Button>
  </PopperTrigger>
  <PopperContent>
    <div class="bg-white p-4 rounded-2 shadow-300 border border-primary">
      <p>This is the popper content.</p>
    </div>
  </PopperContent>
</Popper>

PopperContent is unstyled — wrap it with whatever surface (bg-white, shadow, rounded) the design calls for.

Hover trigger ​

html
<Popper trigger="hover">
  <PopperTrigger>
    <Button>Hover me</Button>
  </PopperTrigger>
  <PopperContent>…</PopperContent>
</Popper>

For tooltips specifically, prefer <TextTooltip> / <RichTextTooltip> — they wrap this same primitive with the standard tooltip surface.

Placement ​

html
<Popper placement="top">…</Popper>
<Popper placement="bottom">…</Popper>

placement accepts any value from @popperjs/core's Placement type:

top · top-start · top-end · bottom · bottom-start · bottom-end · left · left-start · left-end · right · right-start · right-end

By default, the popper flips to the opposite side when there isn't room. Disable with :allow-flip="false".

Content taller than the window ​

PopperContent sets --popper-available-height on the floating panel: the vertical room the content has before it would run past the edge of the window. Content that can outgrow the screen caps itself with it and scrolls inside — that is what keeps a long Menu from being cut off by the bottom of the window.

html
<PopperContent>
  <div class="max-h-(--popper-available-height) overflow-y-auto …">…</div>
</PopperContent>

For a top/bottom placement the figure is the room on the roomier of the two sides, so capped content lands on that side rather than in whatever sliver is left under a trigger near the bottom of the window. With :allow-flip="false" the popper is pinned to one side, so the figure is that side's own room. Panels that always fit are unaffected: the variable is simply larger than they are.

Controlled open state ​

isOpen is two-way bindable. Use it when something outside PopperTrigger should open or close the popper.

html
<Popper v-model:is-open="isOpen">
  <PopperTrigger>
    <Button>Toggle</Button>
  </PopperTrigger>
  <PopperContent>
    <div class="bg-white p-4 rounded-2 shadow-300 border border-primary">
      <Button @click="isOpen = false">Close from inside</Button>
    </div>
  </PopperContent>
</Popper>

The PopperTrigger and PopperContent slots also expose open, close, and isOpen props if you'd rather drive things from inside the slot:

html
<PopperContent v-slot="{ close }">
  <Button @click="close">Done</Button>
</PopperContent>

Static (no outside-click dismiss) ​

By default, clicking outside the popper closes it. Pass static to keep it open until you call close explicitly — useful for confirmation flows.

html
<Popper static v-model:is-open="isOpen">…</Popper>

ESC still closes a static popper.

Offset and animation ​

html
<Popper :offset="[0, 8]" animate>…</Popper>
  • offset is [skid, distance] — skid shifts along the trigger's main axis, distance pushes the content away from the trigger.
  • animate toggles a small fade + scale on open/close (100ms).

When to reach for Popper ​

  • You're building a custom popover that doesn't fit the Tooltip or Menu shape
  • You need a click-to-open searchable dropdown — see Listbox inside a Popper

When not to use Popper ​

  • Modal dialogs — use Modal
  • Side panels — use Drawer
  • Form-style searchable selects — use Combobox

Popper — Props ​

PropTypeDefaultDescription
isOpenbooleanfalseTwo-way bound open state. Use v-model:is-open.
placementPlacement'bottom-end'Where the content sits relative to the trigger. See list above.
trigger'click' | 'hover''click'How the popper opens.
offset[number, number][0, 0][skid, distance] from the trigger.
allowFlipbooleantrueFlip to the opposite side when there isn't room on the preferred side.
staticbooleanfalseDon't close when the user clicks outside.
zIndexnumber50000Stacking context for the teleported content.
openDelaynumber0Milliseconds before opening (handy for hover triggers).
closeDelaynumber0Milliseconds before closing.
animatebooleanfalseFade + scale the content on open/close.
disabledbooleanfalseTrigger does nothing; popper can't open.

PopperTrigger — Slot ​

SlotSlot propsDescription
default{ isOpen, open, close, disabled }The element users click or hover.

PopperContent — Slot ​

SlotSlot propsDescription
default{ isOpen, open, close }The floating body. Teleports to <body>.

Notes ​

  • Content is teleported to <body>, so it escapes parent overflow: hidden and stacking contexts.
  • Pressing Escape while the popper is open closes it.
  • Aurora's Popper uses @popperjs/core under the hood — every Placement value supported there works here.