Primitives
35

Popover

An anchored panel of arbitrary content — a card in a popup, which is what the note on .aura-popup has always said a popover is. The difference from a tooltip is not the shape but what the reader can do with it: a tooltip is a label and closes when the pointer leaves, a popover holds things to interact with, so it opens on click, keeps focus, and closes on Escape or a pointerdown outside. Attaching it is the one thing each package spells its own way — React clones the trigger element, Vue the vnode, Svelte hands the trigger props to spread, Angular is a directive taking the panel as a template — and none of them adds an element: a wrapper would port everywhere and sit between the trigger and its own layout.

plain markup — no library on the page
rendered by aura-react
rendered by aura-react
rendered by aura-react
rendered by aura-react

Code

index.html
<button type="button" class="aura-btn aura-btn--secondary"
        aria-haspopup="dialog" aria-expanded="true" aria-controls="p1">Filters</button>

<div id="p1" role="dialog" aria-label="Filters" data-state="open" class="aura-popup aura-popover aura-card">
  <p class="aura-text aura-text--body">Anything can go here.</p>
</div>
Demo.tsx
import { Button, Popover, Stack, Typography } from '@ilomee/aura-react';

/**
 * Click the trigger. The panel is portalled to `document.body` and positioned by `place()`, so
 * it flips when it does not fit and follows a scroll — and it closes on Escape or on a
 * pointerdown anywhere but itself and the trigger.
 */
export default function Demo() {
  return (
    <Popover
      label="Filters"
      content={
        <Stack gap={2}>
          <Typography variant="h3">Filters</Typography>
          <Typography variant="body" tone="muted">
            Anything can go here — a popover holds things to interact with, which is the half a
            tooltip cannot have.
          </Typography>
        </Stack>
      }
    >
      <Button variant="secondary">Filters</Button>
    </Popover>
  );
}
Demo.vue
<script setup lang="ts">
import { Popover, Button } from '@ilomee/aura-vue';
</script>

<template>
  <Popover label="Filters">
    <template #content><p>Anything can go here.</p></template>
    <Button variant="secondary">Filters</Button>
  </Popover>
</template>
Demo.svelte
<script lang="ts">
  import { Popover } from '@ilomee/aura-svelte';
</script>

<Popover label="Filters">
  {#snippet trigger(props)}
    <button type="button" class="aura-btn aura-btn--secondary" {...props}>Filters</button>
  {/snippet}
  <p>Anything can go here.</p>
</Popover>

Used as [auraPopover] — a directive you put on your own element.

demo.component.ts
<button auraButton variant="secondary" [auraPopover]="panel" label="Filters">Filters</button>
<ng-template #panel><p auraText>Anything can go here.</p></ng-template>

The directive removes its own auraPopover attribute in ngOnInit, so the rendered trigger matches the other three exactly.

Props

Plain markup has no props — the classes below are the whole API. The four packages do nothing more than set these same classes for you.
NameTypeDefaultNotes
content*ReactNodeWhat goes in the panel. Anything — this is the half a tooltip cannot have.
children*ReactElementThe control that opens it. Cloned, not wrapped.
labelstringThe panel’s accessible name. A dialog without one is announced as an unlabelled group — the reader is told something opened and not what.
openOn'click' | 'hover''click'What opens it. hover opens on the pointer *and* on focus, and closes a moment after both leave the trigger and the panel — the panel is portalled to <body> and offset from its trigger, so the pointer crosses bare page to reach it and a close on the first pointerleave would make the panel unreachable by the gesture that opened it. Click still toggles, because a touch device has no hover. A hover popover does not trap focus, where a click one does: a trap is right for a panel someone asked for and hostile for one that appeared under the pointer. Escape and a pointerdown outside still close it.
opentwo-wayboolean
defaultOpenbooleanfalse
onOpenChangeevent(open: boolean) => void
side'top' | 'right' | 'bottom' | 'left''bottom'
align'start' | 'center' | 'end''start'
NameTypeDefaultNotes
defaultslotslotThe trigger. Exactly one element, cloned.
contentslotslotThe panel’s content.
labelstring
openOn'click' | 'hover''click'What opens it. hover opens on the pointer *and* on focus, and closes a moment after both leave the trigger and the panel — the panel is portalled to <body> and offset from its trigger, so the pointer crosses bare page to reach it and a close on the first pointerleave would make the panel unreachable by the gesture that opened it. Click still toggles, because a touch device has no hover. A hover popover does not trap focus, where a click one does: a trap is right for a panel someone asked for and hostile for one that appeared under the pointer. Escape and a pointerdown outside still close it.
opentwo-wayboolean
update:openevent(open: boolean) => void
side'top' | 'right' | 'bottom' | 'left''bottom'
align'start' | 'center' | 'end''start'
NameTypeDefaultNotes
trigger*Snippet<[TriggerProps]>Handed the props to spread onto the control. Svelte has no clone, and spreading is what keeps the markup free of a wrapper.
children*SnippetThe panel’s content.
labelstring
openOn'click' | 'hover''click'What opens it. hover opens on the pointer *and* on focus, and closes a moment after both leave the trigger and the panel — the panel is portalled to <body> and offset from its trigger, so the pointer crosses bare page to reach it and a close on the first pointerleave would make the panel unreachable by the gesture that opened it. Click still toggles, because a touch device has no hover. A hover popover does not trap focus, where a click one does: a trap is right for a panel someone asked for and hostile for one that appeared under the pointer. Escape and a pointerdown outside still close it.
opentwo-wayboolean
side'top' | 'right' | 'bottom' | 'left''bottom'
align'start' | 'center' | 'end''start'
NameTypeDefaultNotes
auraPopover*TemplateRefThe panel’s content, as a template. An <ng-template> renders nothing of its own, which is what keeps the markup identical to the other three.
labelstring
openOn'click' | 'hover''click'What opens it. hover opens on the pointer *and* on focus, and closes a moment after both leave the trigger and the panel — the panel is portalled to <body> and offset from its trigger, so the pointer crosses bare page to reach it and a close on the first pointerleave would make the panel unreachable by the gesture that opened it. Click still toggles, because a touch device has no hover. A hover popover does not trap focus, where a click one does: a trap is right for a panel someone asked for and hostile for one that appeared under the pointer. Escape and a pointerdown outside still close it.
opentwo-wayboolean
openChangeevent(open: boolean) => void
side'top' | 'right' | 'bottom' | 'left''bottom'
align'start' | 'center' | 'end''start'

Emitted classes

Every framework emits the same classes — that is what makes the four libraries look identical. You can use them directly, without any component library.

ClassRoleNotes
.aura-popupbaseRequired. A modifier does nothing without it.
.aura-popoverbaseRequired. A modifier does nothing without it.
.aura-cardbaseRequired. A modifier does nothing without it.

Tokens and rules

components.css
layer
var(--z-dropdown) — under a modal, on purpose
width
min(360px, calc(100vw - 16px)) — its content’s, not its anchor’s
surface
.aura-card — no block of its own