@get-set/gs-tooltip
v1.0.2
Published
Get-Set Tooltip
Maintainers
Readme
@get-set/gs-tooltip
A dependency-free, fully accessible tooltip / popover plugin available in two flavours from one codebase:
- Native / vanilla JS — a
window.GSTooltip(selectorOrElement, params)factory (also exposed as a jQuery plugin and anHTMLElement.prototypemethod), plus zero-config auto-init for any[data-gs-tooltip]element. - React — a
<GSTooltip />forwardRefcomponent that wraps a single child and renders the floating tooltip through a portal.
Both surfaces drive one shared core (actions/, constants/, helpers/,
types/), so behaviour is identical across the two — every option works in
both targets (except the few tagged React-only / native-only below).
Features
- 12 RTL-aware placements —
top,bottom,left,right, each with-start/-endalignment variants, with auto-flip on viewport collision and auto-shift to stay on screen. - 5 entrance/exit animations —
fade,scale,shift-away,shift-toward,perspective(plusnone), each honoringprefers-reduced-motion. - Combinable triggers —
hover,focus,click,manual(Bootstrap-style'hover focus'strings, arrays, or single values). - Smooth follow-cursor —
true/'horizontal'/'vertical'/'initial'. - Interactive tooltips — keep open while the pointer is over the box, with a configurable hover bridge.
- Per-phase delays —
showDelay/hideDelay, or thedelayshorthand. - Arrow — toggleable, size-configurable, auto-centered and clamped.
- Theming —
dark/light/auto(followsprefers-color-scheme) /custom, plus anaccentColor, and richglass/gradient/elevatedsurface classes. - First-class a11y —
role="tooltip", automaticaria-describedbywiring, and Escape-to-close. - Robust positioning —
appendToany container,boundaryPadding, skidding / distanceoffset, sticky reposition on scroll/resize, and hide-on-scroll-out-of-view. - Touch support,
disabledstate,hideOnClick, RTL, per-partcustomClassoverrides, and a React-only scoped-stylesgsxprop.
Compatibility
| Target | Requirement |
|---|---|
| React (the <GSTooltip> component) | React 16.8+ (Hooks + forwardRef), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. |
| Native / vanilla (window.GSTooltip) | No framework. Any modern evergreen browser. Optional jQuery ($(...).GSTooltip(...)) and HTMLElement.prototype.GSTooltip(...) adapters are registered automatically by the bundle. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import server-side (the portal is client-gated). The tooltip is browser-only, so render it inside a Client Component ('use client'). |
The peer range is
^16.8.0 || ^17.0 || ^18.0 || ^19.0, withreact/react-dommarked optional so the native build has zero dependencies.
Project layout
GSTooltip.ts # native entry (webpack -> dist-js/bundle.js, window global)
components/GSTooltip.tsx # React component (tsc -> dist/, npm entry)
actions/ # shared engine (init/show/hide/update/animate/refresh/destroy)
constants/ # animations, placements, default params
helpers/ # layout, content, position, ui helpers
types/ # Params / Ref / Window augmentation
components/styles/ # SCSS + compiled CSS + CSS-as-TS (runtime injection for React)
styles/ # SCSS + compiled CSS (for <link> use by the native build)Install
npm install @get-set/gs-tooltipBuild & test
npm install
npm run build # builds both targets
npm run build:js # native bundle -> dist-js/bundle.js
npm run build:react # React + types -> dist/
npm test # vitestSee example.html for a runnable native showcase exercising placements,
animations, triggers, follow-cursor, themes, and the imperative API.
Usage — Native JS
Load the bundle and the stylesheet, then construct the tooltip. The factory accepts either a CSS selector string or a DOM element as its first argument.
<link rel="stylesheet" href="node_modules/@get-set/gs-tooltip/styles/GSTooltip.css" />
<script src="node_modules/@get-set/gs-tooltip/dist-js/bundle.js"></script>
<button id="save">Save</button>
<script>
// (a) by selector string
GSTooltip('#save', {
reference: 'save-tip',
content: 'Save your changes',
placement: 'top',
animation: 'scale'
});
// (b) by element — both forms are supported
const el = document.getElementById('save');
GSTooltip(el, { reference: 'save-tip-2', content: 'Same, via element' });
</script>The factory returns a ref object with the full imperative API:
const tip = GSTooltip('#save', { reference: 'save', content: 'Hi', trigger: 'manual' });
tip.show();
tip.hide();
tip.toggle();
tip.isVisible(); // boolean
tip.setContent('Updated');
tip.setProps({ theme: 'light', placement: 'bottom' });
tip.enable();
tip.disable();
tip.isEnabled(); // boolean
tip.update(); // recompute position
tip.refresh(); // rebuild from current params
tip.destroy(); // tear down + unregisterAuto-init via [data-gs-tooltip]
Any element carrying data-gs-tooltip is initialized automatically on load — no
JavaScript required. data-gs-placement and data-gs-theme are read too, and a
native title is promoted to the content (and removed to avoid a double tip).
<button data-gs-tooltip="Auto tooltip" data-gs-placement="bottom" data-gs-theme="light">
Hover me
</button>The window.GSTooltipConfigue registry
Every instance is registered under its reference (a GUID is generated when you
omit one):
GSTooltip('#save', { reference: 'save', content: 'Hi' });
const tip = window.GSTooltipConfigue.instance('save'); // -> the ref
tip.show();
window.GSTooltipConfigue.references; // -> [{ key, ref }, ...]Usage — jQuery
The bundle auto-registers $.fn.GSTooltip when jQuery is present. It passes the
element to the factory for each matched node:
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@get-set/gs-tooltip/dist-js/bundle.js"></script>
<script>
$('.has-tip').GSTooltip({ content: 'From jQuery', placement: 'right' });
</script>Usage — HTMLElement.prototype
The bundle also registers HTMLElement.prototype.GSTooltip, so any element can
tooltip itself (the element is passed to the factory):
document.querySelector('#save').GSTooltip({
content: 'From the prototype method',
trigger: 'hover focus'
});Usage — React
<GSTooltip> wraps a single child (the reference element) and portals the
floating tooltip out. Pass options as props; drive it imperatively via ref.
'use client';
import { useRef } from 'react';
import GSTooltip, { GSTooltipHandle } from '@get-set/gs-tooltip';
export default function Demo() {
const tip = useRef<GSTooltipHandle>(null);
return (
<>
<GSTooltip
ref={tip}
content="Save your changes"
placement="top"
animation="scale"
trigger="hover focus"
theme="auto"
arrow
interactive>
<button>Save</button>
</GSTooltip>
<button onClick={() => tip.current?.show()}>Show programmatically</button>
</>
);
}content may be a string, an HTML string (with allowHTML), or any React node:
<GSTooltip content={<strong>Rich <em>node</em> content</strong>}>
<span>hover</span>
</GSTooltip>The ref handle — GSTooltipHandle
| Method | Signature | Description |
|---|---|---|
| show | () => void | Show the tooltip. |
| hide | () => void | Hide the tooltip. |
| toggle | () => void | Toggle visibility. |
| isVisible | () => boolean | Whether the tooltip is currently visible. |
| setContent | (content: ReactNode) => void | Replace the tooltip content. |
| update | () => void | Recompute the tooltip position. |
| enable | () => void | Enable triggers. |
| disable | () => void | Disable triggers (hides if open). |
| isEnabled | () => boolean | Whether triggers are enabled. |
| destroy | () => void | Tear down: unmount the tooltip and unregister it. |
Controlled vs. uncontrolled
// Uncontrolled (default): triggers drive it; defaultVisible sets the initial state.
<GSTooltip content="x" defaultVisible><button>x</button></GSTooltip>
// Controlled: you own the `visible` prop.
<GSTooltip content="x" visible={open} onShow={...} onHide={...}>
<button>x</button>
</GSTooltip>React-only gsx (scoped styles)
gsx injects a <style> scoped to this instance via [data-key], added on
mount and removed on unmount:
<GSTooltip
content="Branded"
theme="custom"
gsx={{
'.gs-tooltip-content': { background: '#7c3aed', color: '#fff' },
'.gs-tooltip-arrow': { background: '#7c3aed' }
}}>
<button>Hover</button>
</GSTooltip>Props
Every option works in both the native params object and as a React prop,
unless tagged. Native-only options resolve elements; React derives the reference
from the wrapped child.
| Prop | Type | Default | Description |
|---|---|---|---|
| reference | string | auto GUID | Stable key used in the registry. |
| content | string (native) / ReactNode (React) | '' | Tooltip content. |
| allowHTML | boolean | false | Treat a string content as raw HTML instead of escaped text. |
| placement | GSTooltipPlacement | 'top' | Preferred placement (12 variants). |
| flip | boolean | true | Auto-flip to the opposite side on viewport collision. |
| shift | boolean | true | Shift along the side to stay in the viewport. |
| boundaryPadding | number | 8 | Padding (px) kept between the tooltip and the viewport edge. |
| offset | { skidding?: number; distance?: number } | { skidding: 0, distance: 8 } | Shift along the side / gap from the reference. |
| trigger | GSTooltipTrigger | 'hover focus' | How the tooltip opens (combinable). |
| showDelay | number | 0 | Delay (ms) before showing. |
| hideDelay | number | 0 | Delay (ms) before hiding. |
| delay | number \| { show?: number; hide?: number } | — | Combined delay shorthand (overrides showDelay/hideDelay). |
| hideOnClick | boolean | true | Hide when the reference / outside is clicked. |
| interactive | boolean | false | Keep open while the pointer is over the tooltip. |
| interactiveBorder | number | 6 | Hover-bridge padding (px) for interactive tooltips. |
| followCursor | boolean \| 'horizontal' \| 'vertical' \| 'initial' | false | Follow the cursor while hovering. |
| touch | boolean \| 'hold' | true | Touch support (tap, or long-press with 'hold'). |
| animation | GSTooltipAnimation \| false | 'fade' | Entrance/exit animation. false disables motion. |
| animationDuration | number \| { enter?: number; exit?: number } | 200 | Animation duration in ms. |
| animationEasing | string | cubic-bezier(.16,1,.3,1) | CSS easing for the animation. |
| reducedMotion | 'auto' \| boolean | 'auto' | Honor prefers-reduced-motion. |
| arrow | boolean | true | Show the pointing arrow. |
| arrowSize | number | 8 | Arrow size in px. |
| theme | 'dark' \| 'light' \| 'auto' \| 'custom' | 'auto' | Visual theme. |
| accentColor | string | — | Accent / background color (CSS var --gst-bg). |
| maxWidth | number \| string | 300 | Max width. Number = px; string = raw CSS. |
| zIndex | number | 9999 | z-index of the floating tooltip. |
| rtl | boolean | false | Right-to-left layout. |
| customClass | { box?, content?, arrow? } | — | Per-part class overrides. |
| className | string | — | Extra class on the tooltip box. |
| appendTo | 'body' \| 'parent' \| string \| HTMLElement \| ref | 'body' | Where to append the floating tooltip. |
| disabled | boolean | false | Start disabled (no triggers fire). |
| aria | boolean | true | Wire aria-describedby on the reference. |
| hideOnScroll | boolean | true | Hide when the reference scrolls out of view. |
| sticky | boolean | true | Reposition on scroll/resize while open. |
| target | selector / element / ref (native) | the constructed element | Explicit reference element(s). |
| gsx | NestedCSS (React-only) | — | Scoped inline styles injected for this instance. |
| visible | boolean (React-only) | — | Controlled visibility. |
| defaultVisible | boolean (React-only) | false | Uncontrolled initial visibility. |
| beforeShow | () => void \| boolean | — | Fired before showing. Return false to cancel. |
| afterShow | () => void | — | Fired after the show transition is set up. |
| beforeHide | () => void \| boolean | — | Fired before hiding. Return false to cancel. |
| afterHide | () => void | — | Fired after the tooltip has fully hidden. |
| onPosition | (placement) => void | — | Fired whenever the tooltip is repositioned. |
| onShow | () => void (React-only) | — | Fired when the tooltip shows. |
| onHide | () => void (React-only) | — | Fired when the tooltip hides. |
Catalogs
Placements (placement, 12)
top, top-start, top-end, bottom, bottom-start, bottom-end,
left, left-start, left-end, right, right-start, right-end.
The -start / -end variants align the tooltip to the start / end edge of the
reference along the cross axis (RTL-aware).
Animations (animation)
| Name | Motion |
|---|---|
| fade | Opacity only. |
| scale | Opacity + scale from 0.85. |
| shift-away | Slides in away from the reference side. |
| shift-toward | Slides in toward the reference side. |
| perspective | 3D perspective tilt + lift. |
| none | No motion (also the reduced-motion fallback). |
Triggers (trigger)
hover, focus, click, manual — combine with a space/comma string
('hover focus'), an array (['click', 'focus']), or a single value. manual
disables all built-in triggers (control via the imperative API only).
Themes (theme)
dark, light, auto (follows prefers-color-scheme), custom (chrome comes
entirely from accentColor / gsx / customClass). Add a gs-tooltip-glass,
gs-tooltip-gradient, or gs-tooltip-elevated class (via className /
customClass.box) for the rich surface variants.
follow-cursor (followCursor)
true (both axes), 'horizontal' (X only), 'vertical' (Y only), 'initial'
(positioned at the pointer once on open, then anchored), false (anchored to
the reference).
License
ISC © Get-Set
