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.
Anything can go here.
Code
<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>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>
);
}<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><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.
<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
| Name | Type | Default | Notes |
|---|---|---|---|
| content* | ReactNode | — | What goes in the panel. Anything — this is the half a tooltip cannot have. |
| children* | ReactElement | — | The control that opens it. Cloned, not wrapped. |
| label | string | — | The 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-way | boolean | — | |
| defaultOpen | boolean | false | |
| onOpenChangeevent | (open: boolean) => void | — | |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | |
| align | 'start' | 'center' | 'end' | 'start' |
| Name | Type | Default | Notes |
|---|---|---|---|
| defaultslot | slot | — | The trigger. Exactly one element, cloned. |
| contentslot | slot | — | The panel’s content. |
| label | string | — | |
| 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-way | boolean | — | |
| update:openevent | (open: boolean) => void | — | |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | |
| align | 'start' | 'center' | 'end' | 'start' |
| Name | Type | Default | Notes |
|---|---|---|---|
| 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* | Snippet | — | The panel’s content. |
| label | string | — | |
| 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-way | boolean | — | |
| side | 'top' | 'right' | 'bottom' | 'left' | 'bottom' | |
| align | 'start' | 'center' | 'end' | 'start' |
| Name | Type | Default | Notes |
|---|---|---|---|
| auraPopover* | TemplateRef | — | The 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. |
| label | string | — | |
| 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-way | boolean | — | |
| 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.
| Class | Role | Notes |
|---|---|---|
| .aura-popup | base | Required. A modifier does nothing without it. |
| .aura-popover | base | Required. A modifier does nothing without it. |
| .aura-card | base | Required. 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