Popover#

A non-modal surface anchored to its trigger — a filter panel, a colour picker, a settings flyout. The popup carries popover="auto", so the platform supplies the top layer, light dismiss and Escape; zero mirrors the model into showPopover() / hidePopover(), positions the popup against the trigger, and moves focus into it on open.

Import#

TSX
import { Popover } from '@sigx/zero/popover';

Popover is a compound: Popover.Root, Popover.Trigger, Popover.Popup, Popover.Title, Popover.Close. It is also re-exported from the @sigx/zero root, together with popoverAnatomy and usePopoverContext.

Usage#

TSX
import { component } from 'sigx';
import { Popover } from '@sigx/zero/popover';

const Filters = component(({ signal }) => {
    const state = signal({ open: false });

    return () => (
        <Popover.Root model={() => state.open} placement="bottom-start">
            <Popover.Trigger color="neutral">Filters</Popover.Trigger>
            <Popover.Popup>
                <Popover.Title>Filters</Popover.Title>

                <Popover.Close>Done</Popover.Close>
            </Popover.Popup>
        </Popover.Root>
    );
});

model={() => state.open} binds the open state both ways — the trigger toggles it, a light dismiss or Escape writes it back through the native toggle event, and writing state.open shows or hides the popup. Leave the model off and pass defaultOpen to keep the state inside the component; openChange fires either way. See Models.

Popover.Root renders no element of its own — it only provides context — so the trigger and the popup can sit anywhere below it. The popup is rendered closed in place and lifted into the top layer when it opens; there is no Portal and no z-index to manage.

Placement and offset#

TSX
<Popover.Root defaultOpen={false} placement="right-start" offset={12}>

placement is one of the fourteen names in the placement vocabulary — top, bottom, left, right, each alone or with -start / -end, plus the logical start and end. The position behavior flips to the opposite side when there is no room and stamps the resolved placement as data-placement on the popup, so a recipe pointing an arrow at the trigger reads the attribute, not the prop. offset is the gap in pixels. See Positioning.

A custom position strategy#

TSX
<Popover.Root positionStrategy={myStrategy}>

positionStrategy swaps the default fixed-coordinates strategy for your own PositionStrategy — an object whose apply(anchor, floating, opts) positions the popup and returns a cleanup. CSS anchor positioning, a floating-ui adapter, or a test double all fit the shape.

Rendering the trigger as your own element#

TSX
<Popover.Trigger asChild>
    {(p) => <a href="#filters" {...p}>Filters</a>}
</Popover.Trigger>

With asChild the default slot receives the part's attribute bag; spread it so the anatomy, the ARIA and the anchor ref land on your element.

Anatomy#

PartElementStatesFlagsNotes
triggerbuttonopen | closeddisabled, focus-visible, pressed, press-animatingaria-haspopup="dialog", aria-expanded, aria-controls. The anchor. Carries the variant axes. asChild.
popupdivopen | closedpopover="auto", role="dialog", tabIndex=-1, data-placement (resolved), aria-labelledby while a Title renders.
titleh3Inside popup.
closebuttondisabled, focus-visible, pressed, press-animatingCloses the popover. Inside popup. asChild.

Popover's Root renders a fragment, so there is no root part. The trigger is the carrier part: the variant axes live on Popover.Trigger, and a design system's axis rules for the scope anchor there. The popup is a top-layer sibling of the trigger, so colour styles the trigger, never the popup.

The popup is a role="dialog", and so receives focus on open — the first tabbable inside it, or the popup itself — and focus returns to the trigger on close. ARIA wiring is presence-aware: the popup references its Title id only while that part is rendered. Trigger and close publish press feedback. See The anatomy contract.

Props#

Popover.Root#

PropTypeDefaultDescription
modelbooleanTwo-way binding of the open state.
defaultOpenbooleanfalseInitial state when uncontrolled.
openChangeevent (open: boolean)Fires whenever the open state changes.
placementPlacement'bottom'Preferred side and alignment; the resolved value renders as data-placement on the popup.
offsetnumber6Gap between the trigger and the popup, in px.
positionStrategyPositionStrategyfixed-coordinatesReplaces the positioning implementation.

Popover.Trigger#

PropTypeDefaultDescription
disabledbooleanfalseRenders data-disabled.
color / size / variant / axes / modsdesign-system vocabularyThe variant axes for the scope, rendered on the trigger.
asChildbooleanfalseRender through the default slot, which receives the part bag.
classstringExtra classes.

Popover.Popup, Popover.Title#

Only class.

Popover.Close#

PropTypeDefaultDescription
disabledbooleanfalseRenders data-disabled.
asChildbooleanfalseRender through the default slot.
classstringExtra classes.

Keyboard and dismissal#

InteractionAction
Enter / Space / click on the triggerToggle the popover.
EscapeCloses (native popover="auto" behaviour); focus returns to the trigger.
Click outsideLight dismiss, from the platform.
TabMoves freely — the popup is non-modal; focus starts inside it on open.

In the shipped design systems#

@sigx/zero-basic and @sigx/zero-daisyui both wire color (the eight recommended roles) and size (xsxl) on the popover trigger, in each skin's own button idiom. Neither wires a variant or any mods on the scope. The popup, title and close parts are styled by every shipped design system, including the open / closed transition on popup.

Tooltip for a hover label with no interactive content · Menu for a list of actions · Dialog when the surface must be modal.