Popover

Non-modal anchored overlay with role="dialog", positioning via @sometic/positioning, and Escape / outside-press dismiss on the DOM controller and sometic-popover. React and Vue adapters are resolve-only open shells (unlike Dialog, which wires createDialogController).

PreviewAnchored non-modal surface

Usage

tsx
import { useState } from "react";
import { Popover } from "@sometic/react/overlay";

export function Example() {
    const [open, setOpen] = useState(true);
    return (
        <>
            <button type="button" onClick={() => setOpen((v) => !v)}>
                Toggle
            </button>
            <Popover open={open}>Filter panel</Popover>
        </>
    );
}
tsx
import { useState } from "react";
import { Popover } from "@sometic/react/overlay";

export function Example(): JSX.Element {
    const [open, setOpen] = useState(true);
    return (
        <>
            <button type="button" onClick={() => setOpen((v) => !v)}>
                Toggle
            </button>
            <Popover open={open}>Filter panel</Popover>
        </>
    );
}
html
<script type="module">
    import { registerOverlayElements } from "@sometic/elements/overlay";
    registerOverlayElements();
</script>

<button type="button" id="trigger">Open</button>
<sometic-popover id="panel" placement="bottom-start" open> Filter panel </sometic-popover>

Vue

vue
<script setup>
import { ref } from "vue";
import { Popover } from "@sometic/vue/overlay";

const open = ref(true);
</script>

<template>
    <button type="button" @click="open = !open">Toggle</button>
    <Popover :open="open">Filter panel</Popover>
</template>

Beta honesty: React/Vue Popover only calls resolvePopover when open is true and returns null when closed. Positioning, portal, outside dismiss, and Escape live on createPopoverController / sometic-popover. See Beta maturity.

How it works

  1. Resolve (resolvePopover): pure view model with role="dialog", data-slot="root", data-state, data-placement, absolute positioning style hooks, optional x / y.
  2. Controller (createPopoverController): non-modal createOverlayController (outside press dismisses, Escape dismisses), computePosition against getTrigger / getContent, optional portalId.
  3. Adapters: React/Vue render a div with resolve attrs when open; no controller wiring (contrast Dialog).
  4. Custom element: sometic-popover owns the controller, reflects open / placement / shadow, emits open-change.

Behavior engines stay in @sometic/dom; frameworks only bind props when using the thin shell.

Anatomy

Partdata-slotRole
Panelroot (resolve) / panel (CE)Floating dialog content
Trigger(app-owned)Pass getTrigger on the DOM controller

Resolve attrs when open: role="dialog", data-state="open"|"closed", data-placement.

Props / attributes

React PopoverProps

HTMLAttributes<HTMLDivElement> plus:

PropTypeDefaultDescription
openbooleanfalseWhen false, React returns null
childrenReactNodePanel content
Native div attrsForwarded to the panel when open

Engine / controller also: placement, offset, portalId, getContent, getTrigger, styling (unstyled, classes, …).

Vue

Prop: open (boolean). Default slot is panel content. No update:open on the thin shell; drive open yourself.

Custom element (sometic-popover)

Observed: open, placement, shadow. Event: open-change{ open: boolean }.

Events / callbacks

SurfaceEvent
React / VueDrive open yourself (no onOpenChange on the thin shell)
CE / controlleronOpenChange / open-change

Controlled vs uncontrolled

Controller supports open / defaultOpen / onOpenChange. React/Vue shells are controlled via open only.

Form participation

N/A as a control. The panel may host form fields; those fields participate normally.

Accessibility

ConcernBehavior
Rolerole="dialog" (non-modal; not aria-modal like Dialog)
FocusNot trapped on controller path; manage focus intentionally
KeyboardEscape dismisses on controller / CE
Outside pressDismisses on controller / CE (non-modal)
NameProvide accessible name for panel content (aria-label / labelledby)
Menu patternNot a Menu; use Menu for menuitem keyboard models

React/Vue shells do not install dismiss or focus management; wire CE or createPopoverController for production a11y behavior.

Styling

Unstyled panel. Useful selectors:

  • [data-slot="root"] / CE panel
  • [data-state="open"|"closed"]
  • [data-placement="…"]

You own arrow, backdrop, and motion if needed.

Edge cases

  • React/Vue without controller: no auto dismiss, no positioning updates; pair with DOM controller or CE for production.
  • Nested popovers: coordinate dismiss layers carefully (outside press can close multiple).
  • SSR: resolve is pure; register CE / create controllers only in the browser.
  • Closed shell: React/Vue return null; do not assume the panel stays mounted.
  • Multi-instance: no module singletons; each controller is independent.
  • Dialog vs Popover: modal focus trap and no outside dismiss belong on Dialog, not Popover.

Performance notes

Position updates on open / scroll / resize via the controller; dispose when done. Prefer the CE or controller when you need live placement. Thin React/Vue shells are cheap resolve-only renders.

When to use / When not

Use for non-modal anchored content (filters, compact forms, contextual panels).

Do not use for:

FAQ

Why is React thinner than Dialog? Dialog adapters call createDialogController. Popover React/Vue remain resolve-only open shells in this beta (Beta maturity).

Does outside click dismiss? Yes on controller / CE (non-modal). Not on the React/Vue shell alone.

Escape? Wired on controller / CE. Shell-only: handle yourself.

Placement values? Positioning package placements (top, bottom-start, …). Resolve default is bottom.

Portal? Controller portalId / overlay portal helpers.

Is this a Menu API? No. Use Menu for menuitem patterns; Popover is a general non-modal overlay shell.

Can the panel hold form fields? Yes. The popover itself is not a form control.

Light DOM or shadow? CE defaults to light DOM; shadow opts into an open shadow root.

Bundle tip? Import @sometic/react/overlay (or Vue / elements / dom subpaths), not a mega barrel.