@get-set/gs-popover
v1.0.2
Published
Get-Set Popover
Maintainers
Readme
@get-set/gs-popover
A rich, accessible, beautifully animated popover — usable four ways from the exact same core:
window.GSPopover(selectorOrElement, params)— the native factory$(selector).GSPopover(params)— the jQuery pluginelement.GSPopover(params)— theHTMLElement.prototypemethod<GSPopover>…</GSPopover>— the React component (with a typedrefhandle)
All four targets share one positioning engine, one animation catalog, one set of constants / helpers / types, and one stylesheet, so behavior is identical no matter how you mount it.
Features
- 12 placements (
top/bottom/left/right×-start/center/-end) plusauto, with flip and shift middleware that keeps the popover in the viewport. - 4 trigger modes —
click,hover(interactive),focus,manual, with configurable open/close delays. - Animation catalog — fade, scale/zoom, slide (4 directions), flip,
shift-away, shift-toward, perspective, blur, bounce (respects
prefers-reduced-motion). - Arrow that points at the trigger, header / title, body, footer, and an optional × close button.
- Dismissal — outside-click and Esc (each independently toggleable), in-content close handlers.
- Focus management — optional focus trap, autofocus, explicit initial focus, and focus return to the trigger on close.
- Theming —
light/dark/auto, accent color, RTL,sameWidth(match the trigger width),maxWidth/minWidth, per-part class overrides. - Optional backdrop with color + blur.
- Accessible —
role,aria-haspopup,aria-expanded,aria-labelledby,tabindex, keyboard support. - React-only
gsx— per-instance scoped styles injected on mount and removed on unmount. - Zero runtime dependencies; React + ReactDOM are optional peers.
Compatibility
| Target | Entry |
| ----------------- | ---------------------------------- |
| Browser / <script> / jQuery / prototype | dist-js/bundle.js (UMD-ish window.GSPopover) |
| Bundler import | dist/components/GSPopover.js (React) |
| Bundler require | dist-js/bundle.js (native) |
| Types | dist/components/GSPopover.d.ts |
React is an optional peer (^16.8 || ^17 || ^18 || ^19). The native targets
work with no framework at all.
Project layout
GSPopover.ts native factory + window.GSPopoverConfigue registry
components/GSPopover.tsx React component (forwardRef + handle + gsx)
actions/ init / open / close / destroy / refresh / animate / focusTrap
helpers/ position (flip/shift) / layout / content / uihelpers
constants/ placements / animations / defaultParams
types/ params / ref / global.d.ts
styles/ GSPopover.css + .scss
components/styles/ GSPopover.css + .scss + GSPopoverCSS.ts (inlined)
example.html native showcaseInstall
npm i @get-set/gs-popoverBuild & test
npm install
npm run build # build:js (webpack bundle) + build:react (tsc)
npm test # vitest (logic + native + React)npm run build produces:
dist-js/bundle.js— the native bundle. A webpackBannerPluginappends an IIFE that registers$.fn.GSPopover(when jQuery is present) andHTMLElement.prototype.GSPopover.dist/— the React component + shared modules compiled with type declarations.
Usage — Native JS
<link rel="stylesheet" href="node_modules/@get-set/gs-popover/styles/GSPopover.css" />
<script src="node_modules/@get-set/gs-popover/dist-js/bundle.js"></script>
<button id="help">Help</button>
<div id="tip" hidden>The quick brown fox.</div>
<script>
// The factory accepts a CSS SELECTOR STRING …
const pop = GSPopover('#tip', {
reference: 'tip',
trigger: '#help',
placement: 'bottom',
title: 'Quick help',
closeButton: true,
animation: 'scale'
});
// … OR a DOM ELEMENT / collection as the first argument.
const el = document.getElementById('tip');
GSPopover(el, { trigger: '#help' });
pop.openPopover();
pop.closePopover();
pop.toggle();
</script>Selector or element. The first argument may be a CSS selector string or an
HTMLElement. This matters because the jQuery and prototype adapters call the factory with an element — a selector-only factory would silently break them.
Instance methods (registry)
Every native instance is registered in window.GSPopoverConfigue keyed by its
reference:
const pop = window.GSPopoverConfigue.instance('tip');
pop.openPopover();
pop.getPlacement(); // 'bottom' (or the flipped side)
pop.update(); // recompute position against the trigger
pop.destroy(); // tear down + free the slotUsage — jQuery
Loading the bundle after jQuery registers $.fn.GSPopover:
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@get-set/gs-popover/dist-js/bundle.js"></script>
<script>
$('#tip').GSPopover({
trigger: '#help',
placement: 'top-start',
animation: 'slide-up'
});
</script>$(sel).GSPopover(params) calls new window.GSPopover(thisElement, params) for
every matched element.
Usage — Prototype
The bundle also adds HTMLElement.prototype.GSPopover:
document.querySelector('#tip').GSPopover({
trigger: '#help',
trigger_mode: 'hover'
});Usage — React
The component wraps a trigger child. The popover renders into a
document.body portal when open.
import { useRef } from 'react';
import GSPopover, { GSPopoverHandle } from '@get-set/gs-popover';
import '@get-set/gs-popover/styles/GSPopover.css';
function Example() {
const ref = useRef<GSPopoverHandle>(null);
return (
<GSPopover
ref={ref}
placement="bottom"
triggerMode="click"
title="Quick help"
closeButton
animation="scale"
theme="auto"
body={<p>The quick brown fox jumps over the lazy dog.</p>}
onPlacement={(p) => console.log('resolved', p)}>
<button>Help</button>
</GSPopover>
);
}- The single child element becomes the trigger + anchor (it is cloned with a
ref). Pass an explicit
trigger(selector / element / ref) to anchor elsewhere. - Body content can be supplied as the
bodyprop (React nodes) or thecontentprop (HTML string). - Controlled: pass
open. Uncontrolled: usedefaultOpenand/or therefhandle.
gsx — scoped styles (React-only)
<GSPopover
reference="fancy"
gsx={{ '.gs-popover': { borderRadius: '16px' }, '.gs-popover-title': { color: 'crimson' } }}>
<button>Open</button>
</GSPopover>gsx is injected as a <style data-key="…"> scoped to that instance via
[data-key='…'] and removed automatically on unmount.
Options / props
The same options work in native params and React props (React additionally
accepts triggerMode as an alias of trigger_mode, the body node prop, and
gsx).
| Name | Type | Default | Description |
| ---- | ---- | ------- | ----------- |
| reference | string | auto GUID | Stable key used by the registry. |
| trigger | string \| HTMLElement \| RefObject | wrapped child (React) | Anchor / trigger element(s). |
| trigger_mode / triggerMode | 'click' \| 'hover' \| 'focus' \| 'manual' | 'click' | How the popover opens. |
| closeHandler | string \| HTMLElement \| RefObject | — | In-content element(s) that close on click. |
| placement | GSPopoverPlacement | 'bottom' | One of the 12 placements or 'auto'. |
| flip | boolean | true | Flip to the opposite side when there is no room. |
| shift | boolean | true | Shift along the cross axis to stay in view. |
| offset | number \| { main?: number; cross?: number } | 8 | Distance from the trigger. |
| boundaryPadding | number | 8 | Viewport padding kept while flipping/shifting. |
| sameWidth | boolean | false | Match the popover width to the trigger. |
| reposition | boolean | true | Keep positioned on scroll / resize. |
| openDelay | number | 0 | Delay (ms) before opening on hover/focus. |
| closeDelay | number | 100 | Delay (ms) before closing on hover-leave/blur. |
| interactive | boolean | true | Stay open while the pointer is over the popover (hover mode). |
| title | string | — | Header title (escaped). Renders the header. |
| header | string | — | Full header HTML (overrides title). |
| content | string | — | Body HTML string (native; React uses body/children). |
| body (React) | ReactNode | — | Body content as React nodes. |
| footer | string \| false | — | Footer HTML. false = no footer. |
| arrow | boolean | true | Show the arrow pointing at the trigger. |
| closeButton | boolean | false | Show the built-in × close button. |
| maxWidth | number \| string | 320 | Clamp the popover width. |
| minWidth | number \| string | — | Lower bound on the width. |
| dismissOnOutsideClick | boolean | true | Close on outside click. |
| dismissOnEsc | boolean | true | Close on Escape. |
| animation | GSPopoverAnimation \| false | 'scale' | Entrance animation (false disables motion). |
| exitAnimation | GSPopoverAnimation \| false | mirrors animation | Separate exit animation. |
| animationDuration | number \| { enter?: number; exit?: number } | 200 | Duration(s) in ms. |
| animationEasing | string | cubic-bezier(.16,1,.3,1) | Easing for enter/exit. |
| reducedMotion | 'auto' \| boolean | 'auto' | Honor prefers-reduced-motion. |
| backdrop | boolean | false | Render a dimming backdrop behind the popover. |
| backdropColor | string | — | Backdrop color. |
| backdropBlur | boolean \| number \| string | false | Backdrop blur (true=4px). |
| trapFocus | boolean | false | Trap Tab focus inside the popover. |
| autofocus | boolean | false | Auto-focus the first focusable element on open. |
| initialFocus | string \| HTMLElement \| RefObject \| false | — | Explicit element to focus (false = the container). |
| returnFocus | boolean | true | Restore focus to the trigger on close. |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme. |
| accentColor | string | — | Accent color (CSS var --gsp-accent). |
| rtl | boolean | false | Right-to-left layout. |
| customClass | GSPopoverCustomClass | — | Per-part class overrides. |
| className | string | — | Extra class on the popover root. |
| role | string | 'dialog' | ARIA role for the popover. |
| gsx (React) | NestedCSS | — | Per-instance scoped styles. |
| open (React) | boolean | — | Controlled open state. |
| defaultOpen (React) | boolean | false | Uncontrolled initial open state. |
| beforeOpen | () => void \| boolean | — | Return false to cancel opening. |
| afterOpen | () => void | — | Fired after the open transition. |
| beforeClose | (reason?) => void \| boolean | — | Return false to cancel closing. |
| afterClose | (reason?) => void | — | Fired after the popover fully closes. |
| onOpen (React) | () => void | — | Open callback. |
| onClose (React) | (reason?) => void | — | Close callback. |
| onPlacement | (placement) => void | — | Fired after each reposition with the resolved placement. |
GSPopoverCustomClass parts
popover, arrow, header, title, closeButton, body, footer,
backdrop.
Variant catalogs
Placement catalog
auto, top, top-start, top-end, bottom, bottom-start, bottom-end,
left, left-start, left-end, right, right-start, right-end.
auto chooses the side with the most available space. With flip enabled (the
default), any placement that overflows is flipped to its opposite side when that
side has more room; shift then nudges the popover along the cross axis to stay
inside boundaryPadding.
Animation catalog
| Name | Effect |
| ---- | ------ |
| fade | Opacity only. |
| scale / zoom | Scale from 0.9 + fade (aliases). |
| slide / slide-up | Rise + fade (aliases). |
| slide-down | Drop + fade. |
| slide-left | Slide in from the right + fade. |
| slide-right | Slide in from the left + fade. |
| flip | 3D rotateX hinge + scale. |
| shift-away | Scale up slightly on exit. |
| shift-toward | Scale down slightly on exit. |
| perspective | Z-depth tilt. |
| blur | Blur + scale + fade. |
| bounce | Springy overshoot. |
| none | No-op (also the reduced-motion fallback). |
animation: false disables motion entirely. Set exitAnimation for a different
exit, and animationDuration / animationEasing to tune timing.
Trigger-mode catalog
| Mode | Behavior |
| ---- | -------- |
| click | Toggle on trigger click; dismiss on outside click / Esc. |
| hover | Open on mouseenter/focus, close on leave/blur (interactive). |
| focus | Open on focus, close on blur. |
| manual | Wires nothing — you drive open/close via the API/handle. |
Theme catalog
| Theme | Behavior |
| ----- | -------- |
| light | Forces the light palette. |
| dark | Forces the dark palette. |
| auto | Follows prefers-color-scheme. |
Imperative API
React GSPopoverHandle (via ref)
| Method | Description |
| ------ | ----------- |
| open() | Open the popover. |
| close(reason?) | Close the popover (optional dismissal reason). |
| toggle() | Toggle the open state. |
| isOpen() | boolean — current open state. |
| setContent(node) | Replace the body content (React node). |
| update() | Reposition against the trigger. |
| getPlacement() | The currently-resolved placement (after any flip). |
| destroy() | Close + unregister the instance. |
Native instance (returned by the factory)
| Method | Description |
| ------ | ----------- |
| openPopover() | Open the popover. |
| closePopover(reason?) | Close the popover. |
| toggle() | Toggle the open state. |
| isOpen() | boolean — current open state. |
| update() | Reposition against the trigger. |
| getPlacement() | The currently-resolved placement. |
| setContent(content) | Replace the body (HTML string or element). |
| refresh() | Rebuild from (possibly updated) params. |
| destroy() | Tear down listeners + DOM and free the registry slot. |
Registry — window.GSPopoverConfigue
window.GSPopoverConfigue.references; // Array<{ key, ref }>
window.GSPopoverConfigue.instance(key); // -> ref | undefinedBoth the native factory and the React component register here, so
instance(reference) resolves an instance created either way.
Dismissal reasons
close callbacks receive one of: 'outside', 'esc', 'close-button',
'trigger', 'reference-hidden', 'manual'.
License
ISC © Get-Set
