Popover
Use the popover patch to customize this element.
Customization
Must see the source of patch at the bottom of each patch page to understand the structure then code it still code as html native element.
There are four levels of customization, in increasing order of effort:
- Patch props. Each patch exposes a small, stable set of props—typically fewer than five. Lowest friction.
- Context attributes. Use
dataTone,dataSize, anddataDensityon a container to shift tone, size, or density for an entire subtree without touching individual elements. - Inline override. Native-wins merge strategy: any property set directly on the element overrides the patch value.
- Create a variant. Clone a similar patch and edit it. Use this only when you need a reusable custom version.
Formulas
Unit - U = fontSize / 4 - convert final values with themeSpacing(n).
Size - n = intrinsic text lines, w = wrapping level, d = density factor:
height = (n * 6 + 2 * d * w) * U
paddingBlock = d * w * U
paddingInline = ceil(3 / w) * d * w * U
radius = d * w * U
Base density d = 1.5:
| U | w=0 | w=1 | w=2 | w=3 |
|---|---|---|---|---|
height (n = 1) | 6 | 9 | 12 | 15 |
| paddingBlock | 0 | 1.5 | 3 | 4.5 |
| paddingInline | 3 | 4.5 | 6 | 4.5 |
| radius | 0 | 1.5 | 3 | 4.5 |
Tone - K = N / 2 where N is the palette length. For N = 18, K = 9.
| Role | Shift | n=0 |
|---|---|---|
| Background | parent +/- n | 0 |
| Text | bg + K | 6 |
| Border | bg + K/2 | 3 |
| Hover | bg + 2K/3 | 4 |
| Selected / Focus | above +/- K/3 | 2-4 |
State shift range: K/3 <= delta <= 2K/3.
<div class="blocks">
<div class="block active" data-tab="0">
import {
type DomphyElement,
merge,
type PartialElement,
toState,
type ValueOrState,
} from "@domphy/core";
import type { Placement } from "@domphy/floating";
import { creatFloating } from "../utils/floating.js";
/**
* Floating popover primitive. Attaches to its host as the anchor/trigger and
* shows a floating `content` element (with `role="dialog"`) on click or hover,
* positioned via `@domphy/floating`. Returns the anchor partial, which merges
* trigger wiring (haspopup/expanded, focus/blur dismissal). Apply to the
* trigger element you want the popover anchored to.
*
* @param props - Configuration.
* @param props.openOn - Interaction that opens the popover: `"click"` or `"hover"`. Defaults to `"click"`.
* @param props.open - Open state, accepts a value or `State`. Defaults to `false`.
* @param props.placement - Floating placement (e.g. `"bottom"`, `"top-start"`), value or `State`. Defaults to `"bottom"`.
* @param props.content - The floating content element to display.
* @example { button: "Open", $: [popover({ openOn: "click", content: { div: "Hi" } })] }
*/
function popover(props: {
openOn: "click" | "hover";
open?: ValueOrState<boolean>;
placement?: ValueOrState<Placement>;
content: DomphyElement;
}): PartialElement {
const { open = false, placement = "bottom", openOn = "click" } = props;
let popoverId: string | null = null;
const openState = toState(open);
const placeState = toState(placement);
const { show, hide, anchorPartial } = creatFloating({
open: openState,
placement: placeState,
content: props.content,
});
const popoverPartial: PartialElement = {
role: "dialog",
dataTone: "shift-11",
onMouseEnter: () => openOn === "hover" && show(),
onMouseLeave: () => openOn === "hover" && hide(),
_onInsert: (node) => {
const id = node.attributes.get("id");
popoverId = id || node.nodeId;
!id && node.attributes.set("id", popoverId);
},
};
props.content.$ ||= [];
props.content.$.push(popoverPartial);
const triggerPartial: PartialElement = {
ariaHaspopup: "dialog",
ariaExpanded: (listener) => openState.get(listener),
onMouseEnter: () => openOn === "hover" && show(),
onMouseLeave: () => openOn === "hover" && hide(),
onClick: () => {
if (openOn === "click") {
if (openState.get()) {
hide();
} else {
show();
}
}
},
onFocus: () => show(),
onBlur: (e, node) => {
const related = (e as FocusEvent).relatedTarget as Node | null;
const root = node.getRoot().domElement as Element;
const floatingEl = popoverId
? root.querySelector(`#${CSS.escape(popoverId)}`)
: null;
if (related && floatingEl?.contains(related)) return;
hide();
},
_onMount: (node) =>
popoverId && node.attributes.set("ariaControls", popoverId),
};
merge(anchorPartial, triggerPartial);
return anchorPartial;
}
export { popover };
</div>
</div>