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".

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.