Popover

A click-triggered floating panel (Base UI).

Real use · click-triggered info panel

Glass · frosted panel

anchor · position against another element

$1,284.00

anchor · a virtual element at the pointer

Click anywhere in this box

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-react
import { Popover } from "@wizeworks/silicaui-react";

Props

PropTypeRequiredDescription
childrenReact.ReactNodeno—
sidePopoverSideno—
alignPopoverAlignno—
sideOffsetnumberno—
arrowbooleannoShow the little arrow. Default false.
anchorBasePositionerProps["anchor"]noWhat to position against. Defaults to the component's own trigger.
positionMethodBasePositionerProps["positionMethod"]noabsolute (default) or fixed. Use fixed to escape a clipping ancestor.
alignOffsetBasePositionerProps["alignOffset"]noOffset along the alignment axis, in px. Pairs with align.
collisionBoundaryBasePositionerProps["collisionBoundary"]noThe 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.
collisionPaddingBasePositionerProps["collisionPadding"]noInset from the collision boundary, in px.
collisionAvoidanceBasePositionerProps["collisionAvoidance"]noWhich collision strategy runs per axis (flip, shift, none).
stickyBasePositionerProps["sticky"]noKeep the popup glued to the anchor while it scrolls out of view.
arrowPaddingBasePositionerProps["arrowPadding"]noMinimum distance from the popup's corner to the arrow, in px.
disableAnchorTrackingBasePositionerProps["disableAnchorTracking"]noStop 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=&quot;glass&quot;</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-behaviors
import { 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.