Popover
A click-triggered floating panel (Base UI).
Real use · click-triggered info panel
Glass · frosted panel
anchor · position against another element
anchor · a virtual element at the pointer
Using Popover
Silica ships one design system through three paths. They share a class vocabulary but are not interchangeable — pick the one that matches what you are building.
Plain HTML — CSS classes
The Tailwind v4 plugin. Works in any framework or none — Rails, Django, PHP, Go, a static file. This path ships no JavaScript, so Popover is styled but inert: its behavior comes from popover. For that behavior with no framework, use the node-tree path below.
/* in your CSS */
@import "tailwindcss";
@plugin "@wizeworks/silicaui";Root class popover, with 3 modifiers:
.popover.popover-arrow.popover-description.popover-title
React
Typed components over those same classes, with behavior and accessibility from Base UI.
npm i @wizeworks/silicaui-reactimport { Popover } from "@wizeworks/silicaui-react";Props
| Prop | Type | Required | Description |
|---|---|---|---|
| children | React.ReactNode | no | — |
| side | PopoverSide | no | — |
| align | PopoverAlign | no | — |
| sideOffset | number | no | — |
| arrow | boolean | no | Show the little arrow. Default false. |
| anchor | BasePositionerProps["anchor"] | no | What to position against. Defaults to the component's own trigger. |
| positionMethod | BasePositionerProps["positionMethod"] | no | absolute (default) or fixed. Use fixed to escape a clipping ancestor. |
| alignOffset | BasePositionerProps["alignOffset"] | no | Offset along the alignment axis, in px. Pairs with align. |
| collisionBoundary | BasePositionerProps["collisionBoundary"] | no | The element the popup must stay inside. Defaults to the viewport — set it to a scroll container so the popup collides with the panel, not the window. |
| collisionPadding | BasePositionerProps["collisionPadding"] | no | Inset from the collision boundary, in px. |
| collisionAvoidance | BasePositionerProps["collisionAvoidance"] | no | Which collision strategy runs per axis (flip, shift, none). |
| sticky | BasePositionerProps["sticky"] | no | Keep the popup glued to the anchor while it scrolls out of view. |
| arrowPadding | BasePositionerProps["arrowPadding"] | no | Minimum distance from the popup's corner to the arrow, in px. |
| disableAnchorTracking | BasePositionerProps["disableAnchorTracking"] | no | Stop re-measuring when the anchor moves. Cheaper, but goes stale. |
Source of the demo above
The exact file that renders the examples above, so it can never drift from them. It imports a few of this site’s own layout helpers (../lib/…), so read it for the component calls rather than copying it whole.
Show source
import { useRef, useState } from "react";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverTitle,
PopoverDescription,
Button,
} from "@wizeworks/silicaui-react";
import { Section } from "../lib/Section";
export function PopoverDemo() {
return (
<>
<Section title="Real use · click-triggered info panel">
<Popover>
<PopoverTrigger>
<Button variant="outline" color="neutral">
Storage details
</Button>
</PopoverTrigger>
<PopoverContent arrow>
<PopoverTitle>Storage</PopoverTitle>
<PopoverDescription>
92% of your 100 GB quota is used. Upgrade your plan for more
space.
</PopoverDescription>
</PopoverContent>
</Popover>
</Section>
<Section title="Glass · frosted panel">
<div
className="flex justify-center rounded-box p-16"
style={{
backgroundImage:
"linear-gradient(135deg, var(--color-primary), var(--color-accent), var(--color-secondary))",
}}
>
<Popover>
<PopoverTrigger>
<Button variant="outline" color="neutral">
Storage details
</Button>
</PopoverTrigger>
<PopoverContent className="glass" arrow>
<PopoverTitle>Storage</PopoverTitle>
<PopoverDescription>
<code>PopoverContent className="glass"</code> — the
gradient behind it shows through the blur.
</PopoverDescription>
</PopoverContent>
</Popover>
</div>
</Section>
<ElementAnchor />
<PointerAnchor />
</>
);
}
/**
* `anchor` — position against something OTHER than the trigger. Without it a
* popup can only ever sit against the element that opened it, which breaks the
* common case of a toolbar button that annotates a row, a cell, or a chart mark.
*/
function ElementAnchor() {
const target = useRef<HTMLDivElement>(null);
return (
<Section title="anchor · position against another element">
<div className="flex items-start gap-8">
<Popover>
<PopoverTrigger>
<Button variant="outline" color="neutral" data-demo="anchor-trigger">
Explain the total
</Button>
</PopoverTrigger>
<PopoverContent
anchor={target}
side="right"
arrow
data-demo="anchor-popup"
>
<PopoverTitle>Order total</PopoverTitle>
<PopoverDescription>
The panel is anchored to the figure, not to the button that
opened it.
</PopoverDescription>
</PopoverContent>
</Popover>
<div
ref={target}
data-demo="anchor-target"
className="rounded-box border border-base-300 px-6 py-4 text-2xl font-bold"
>
$1,284.00
</div>
</div>
</Section>
);
}
/**
* A virtual element is just `{ getBoundingClientRect() }` — enough to pin a
* popup to a caret, a pointer, or a spot on a canvas that has no DOM node.
*/
function PointerAnchor() {
const [point, setPoint] = useState<{ x: number; y: number } | null>(null);
return (
<Section title="anchor · a virtual element at the pointer">
<div
data-demo="pointer-surface"
className="grid h-40 place-items-center rounded-box border border-dashed border-base-300"
onClick={(e) => setPoint({ x: e.clientX, y: e.clientY })}
>
Click anywhere in this box
</div>
<Popover open={point !== null} onOpenChange={() => setPoint(null)}>
<PopoverContent
data-demo="pointer-popup"
side="bottom"
align="start"
anchor={
point && {
getBoundingClientRect: () =>
new DOMRect(point.x, point.y, 0, 0),
}
}
>
<PopoverDescription>
Anchored at {point?.x}, {point?.y}
</PopoverDescription>
</PopoverContent>
</Popover>
</Section>
);
}Vanilla — node tree to HTML
popover — anchored trigger/panel pairs; positioning is computed at runtime (see popover.ts), same precedent as carousel's track.style.transform. Build a node tree, project it with toHtml(), and let the zero-dependency runtime hydrate it. This is the path for generated or user-authored documents — a site builder, CMS output, a static export.
npm i @wizeworks/silicaui-html @wizeworks/silicaui-behaviorsimport { atom, toHtml } from "@wizeworks/silicaui-html";
const node = atom("Popover", "popover", { /* props */ }, [/* children */]);
const markup = toHtml(node);Styling on this path is the class string in the second argument — the same classes as the Plain HTML section above. There is no color prop here; write "popover popover-primary" rather than passing a colour as a prop.
- Takes children
- yes
- Hydrated by
popover
Load @wizeworks/silicaui-behaviors on the page or this markup renders correctly and does nothing.
The full API for Popover, on every path, is available through the Silica MCP server.