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).
Usage
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>
</>
);
}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>
</>
);
}<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
<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
Popoveronly callsresolvePopoverwhenopenis true and returnsnullwhen closed. Positioning, portal, outside dismiss, and Escape live oncreatePopoverController/sometic-popover. See Beta maturity.
How it works
- Resolve (
resolvePopover): pure view model withrole="dialog",data-slot="root",data-state,data-placement, absolute positioning style hooks, optionalx/y. - Controller (
createPopoverController): non-modalcreateOverlayController(outside press dismisses, Escape dismisses),computePositionagainstgetTrigger/getContent, optionalportalId. - Adapters: React/Vue render a
divwith resolve attrs whenopen; no controller wiring (contrast Dialog). - Custom element:
sometic-popoverowns the controller, reflectsopen/placement/shadow, emitsopen-change.
Behavior engines stay in @sometic/dom; frameworks only bind props when using the thin shell.
Anatomy
| Part | data-slot | Role |
|---|---|---|
| Panel | root (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:
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | When false, React returns null |
children | ReactNode | Panel content | |
| Native div attrs | Forwarded 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
| Surface | Event |
|---|---|
| React / Vue | Drive open yourself (no onOpenChange on the thin shell) |
| CE / controller | onOpenChange / 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
| Concern | Behavior |
|---|---|
| Role | role="dialog" (non-modal; not aria-modal like Dialog) |
| Focus | Not trapped on controller path; manage focus intentionally |
| Keyboard | Escape dismisses on controller / CE |
| Outside press | Dismisses on controller / CE (non-modal) |
| Name | Provide accessible name for panel content (aria-label / labelledby) |
| Menu pattern | Not 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.