Components · Overlays
Popover
Anchor a floating card of rich content — a title, a body, and an optional arrow — to a trigger.
Examples
Placement
Trigger
Popover · title + content · placement · trigger · arrow · click the trigger
<!-- Paste-and-run: save as .html and open in a browser. No build step.
<aha-popover> is the SAME shared custom element React and Vue consume.
Put the anchor in slot="trigger"; add a `title` header + slotted (or `content`) body.
placement = top | bottom | left | right (centred) OR edge-aligned top-start | top-end |
bottom-start | bottom-end | left-start | left-end | right-start | right-end
(same side, aligned to the trigger's start/end edge, arrow offset near it)
· trigger = click | hover | focus · `arrow` toggles the pointer.
A click trigger closes on outside-click and Escape (focus returns to the trigger); emits composed open / close. -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/tokens.css">
<script type="module">
import 'https://cdn.jsdelivr.net/gh/ahaslides-product/ahaslides-design@master/lib/aha-popover.js'; // registers <aha-popover>
</script>
<!-- bottom-end: anchored under the trigger, aligned to its right edge, arrow near that edge -->
<aha-popover id="pop" placement="bottom-end" trigger="click" arrow title="Share this deck">
<button slot="trigger">Options</button>
<div>
<p style="margin:0 0 8px">Share, rename or export this deck.</p>
<a href="#">Manage access</a>
</div>
</aha-popover>
<script>
document.getElementById('pop').addEventListener('open', () => console.log('opened'));
</script>
import '@ahaslides-product/design/aha-popover'; // registers <aha-popover>
import { useRef, useEffect } from 'react';
// React 18 needs a thin wrapper; React 19 can use <aha-popover> directly.
// placement: top | bottom | left | right (centred) OR edge-aligned top-start | top-end |
// bottom-start | bottom-end | left-start | left-end | right-start | right-end
function AhaPopover({ placement = 'bottom', trigger = 'click', arrow = false, title, onOpenChange, anchor, children }) {
const ref = useRef();
useEffect(() => {
const el = ref.current;
const o = () => onOpenChange?.(true);
const c = () => onOpenChange?.(false);
el.addEventListener('open', o);
el.addEventListener('close', c);
return () => { el.removeEventListener('open', o); el.removeEventListener('close', c); };
});
return (
<aha-popover ref={ref} placement={placement} trigger={trigger} title={title} {...(arrow ? { arrow: '' } : {})}>
<span slot="trigger">{anchor}</span>
{children}
</aha-popover>
);
}
// usage — edge-aligned: aligned to the trigger's right edge, arrow near it
<AhaPopover anchor={<button>Options</button>} title="Share this deck" placement="bottom-end" arrow trigger="click">
<a href="#">Manage access</a>
</AhaPopover>
// main.ts — register the element + mark aha-* as custom elements
import '@ahaslides-product/design/aha-popover';
app.config.compilerOptions.isCustomElement = (tag) => tag.startsWith('aha-');
// Component.vue — placement: top|bottom|left|right (centred) OR edge-aligned
// top-start|top-end|bottom-start|bottom-end|left-start|left-end|right-start|right-end
<template>
<aha-popover placement="bottom-end" trigger="click" arrow title="Share this deck" @open="onOpen" @close="onClose">
<button slot="trigger">Options</button>
<div>
<p>Share, rename or export this deck.</p>
<a href="#">Manage access</a>
</div>
</aha-popover>
</template>
API
| Prop | Type | Default | Notes |
|---|---|---|---|
open | boolean | false | Controlled open state; reflected attribute |
placement | top | bottom | left | right | top-start | top-end | bottom-start | bottom-end | left-start | left-end | right-start | right-end | bottom | Side the panel anchors to. The 4 cardinal values align the panel to the trigger's leading edge; the 8 edge-aligned values (…-start / …-end) pin it to the trigger's start/end edge with the arrow offset near that edge (AntD naming) |
trigger | click | hover | focus | click | How the panel opens. Click closes on outside-click + Escape (focus returns); hover/focus track pointer/focus |
title | string | — | Bold header rendered above the body (hidden when empty) |
content | string | — | Text body alternative to slotted content (used when nothing is slotted) |
arrow | boolean | false | Show the pointer arrow at the panel edge |
[slot=trigger] | slotted element | — | The element that toggles the panel |
open / close | CustomEvent | — | Composed events on show / hide |
Install
# .npmrc — once: point the @ahaslides-product scope at GitHub Packages
@ahaslides-product:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN} # a GitHub token with read:packages
npm i @ahaslides-product/design
import '@ahaslides-product/design/tokens.css'; // once, at the app root
import '@ahaslides-product/design/aha-popover'; // registers <aha-popover>
Agent feed for this component (absolute, fetchable anywhere): popover.agent.json · popover.md · popover.llms.txt
When to use
When to use
- Popover — a small panel of rich or interactive content anchored to a control
- Tooltip — a short, non-interactive hint on hover
- Modal — a focused task that should block the rest of the page
Trigger on click for interactive content (Tooltip owns hover). Give it a short `title` and a one-line body; a long form belongs in a Modal. A click trigger closes on outside-click and Escape (focus returns to the trigger) — both built in.
Surfaces
editor dashboard settings
Spec
Panel bg-elevated #FFFFFF, 1px border #E3E3E3, radius 8, min-width 180 / max 280 · Elevation 0 6px 16px ink 12% · Title SemiBold 600, 14/21 text-default, 6px above the body · Body pad 12 16, 14/21 text-secondary #4A4A4A (text-default when title-less) · Placement top · bottom · left · right (8px offset), PLUS 8 edge-aligned (top-start/-end, bottom-start/-end, left-start/-end, right-start/-end) — same side, aligned to the trigger edge with the arrow offset near it · Trigger click (default) · hover · focus · Arrow optional 8px pointer at the panel edge, matches the surface + border · Motion fade + 4px directional lift on open (ease-out, persistent node)