Popover
Tooltips and popovers with collision-aware anchored positioning.
npm install @devix-labs/popover
tippy.js takes 4,590,050 downloads a week and its repository is archived; its most-reacted open issue is the migration off its dead positioning engine, which will now never happen, and #1183 is that it is broken on React 19. Floating UI, at 70M/wk, is excellent and solves a different problem: where a box goes. It deliberately does not solve what a tooltip is — hover intent, touch, focus, ARIA, dismissal and the rules about staying open — so everybody writes that layer themselves, once per project. This is that layer. The panel lives in the browser's top layer, so there is no z-index in the stylesheet at all and no overflow:hidden ancestor can clip it, which is the entire reason tippy has an appendTo option. WCAG 2.2 SC 1.4.13 has three clauses and each has its own test: Escape dismisses without moving the pointer, a grace period lets you move diagonally onto the panel without it vanishing, and nothing closes on a timer. Touch gets a long press rather than a pretend hover, and the positioning — twelve placements, flip, slide, an arrow that follows the anchor — is 590 bytes of our own arithmetic.
What you get
Nothing can cover it
The panel is in the browser's top layer, so there is no z-index in the stylesheet — not a large one, none. The test puts an element at the maximum z-index on the page and asks the browser what is actually at the panel's centre.
Nothing can clip it
An anchor inside overflow: hidden — a table cell, a card, a scrolling sidebar — shows its panel in full. Tippy's whole appendTo option exists to work around this by moving the element elsewhere in the tree. Here there is nothing to move.
WCAG 2.2 SC 1.4.13, all three clauses
Dismissible: Escape closes it without the pointer moving. Hoverable: you can move diagonally onto the panel without it disappearing under you. Persistent: it does not close on a timer. Each one has its own browser test, because 'accessible' is not a claim you make without one.
Touch is not hover
A phone has no pointer, and pretending it does is why tooltips there open and stick until you tap something unrelated. A long press opens a tooltip and the next tap closes it; a popover opens on tap.
A tooltip is not a popover
One describes its anchor with aria-describedby; the other is a region opened with aria-expanded and aria-controls, and takes focus. Different triggers, different dismissal, different ARIA — so they are two functions rather than one with a flag that hides the difference.
Positioning of our own, 590 bytes
Twelve placements, flipping when there is no room, sliding to stay on screen, and an arrow placed from the anchor rather than the panel so that a slide leaves it still pointing at the right thing. Pure arithmetic, no DOM, exported on its own.
3 kB, and it works on React 19
Which tippy does not — its #1183 is 'Accessing element.ref was removed in React 19', filed against a read-only repository. Here React, Vue, Svelte and a web component are thin wrappers over one DOM core.
Your text stays text
Content is inserted as text unless you pass html: true, because most tooltip content is a name or a row out of a database. A Node goes in as real elements, and a function is called fresh each time it opens.
Popover & Tooltip — overview
Why it exists
tippy.js takes 4,590,050 downloads a week and its repository is archived.
Its most-reacted open issue, #1071 (+32), is "Migrate from Popper (v2) to Floating UI" — the migration that will now never happen. #1183 is "Accessing element.ref was removed in React 19". #1124 is ESM failing under Vitest. #1020 is the dist folder missing from releases. All the marks of an unmaintained package still in four and a half million installs.
Floating UI, at 70 million a week, is excellent and we do not compete with it. It solves where a box goes. It deliberately does not solve what a tooltip is — hover intent, touch, focus, ARIA, dismissal, and the rules about staying open. Everyone who uses it writes that layer themselves, once per project.
The top layer
The panel uses the browser's own popover API, so it is painted above everything.
That removes two entire categories of bug. There is no z-index in the
stylesheet — not a large one, none — because a panel in the top layer cannot be
covered. And an anchor inside overflow: hidden — a table cell, a card, a
scrolling sidebar — shows its panel in full, because the top layer is not
clipped by ancestors.
Tippy's appendTo option exists solely to work around those two problems by
moving the element elsewhere in the tree, with everything that then goes wrong
about positioning and cleanup. Here there is nothing to move.
The test puts the anchor inside an overflow: hidden box on a page with an
element at the maximum z-index, opens the tooltip, and asks the browser what
is actually at the panel's centre.
WCAG 2.2 SC 1.4.13
Content on Hover or Focus has three clauses. Almost nothing implements all three, and each has a test here:
- Dismissible — Escape closes it without moving the pointer, and without the anchor needing focus.
- Hoverable — you can move onto the panel. Going diagonally from the anchor
crosses the gap between them, where a naive implementation closes under you;
a departure heading towards the panel gets a grace period instead. Tippy needs
interactive: trueand a plugin for this. - Persistent — nothing closes on a timer.
And because the rule says hover or focus, a keyboard opens it: focus shows, blur hides.
Touch is not hover
A phone has no pointer. Pretending it does is why tooltips on phones open and then stick until you tap something unrelated. A long press opens a tooltip, the next tap closes it, and a popover opens on tap.
A tooltip is not a popover
They get different ARIA — aria-describedby against aria-expanded plus
aria-controls — different triggers, different dismissal and different focus
behaviour. So they are two functions rather than one with a flag, and the one
you called decides what a screen reader is told.
Positioning, at 590 bytes
Ours, not a dependency: twelve placements, flip when there is no room, slide along the edge to stay on screen, and an arrow placed from the anchor's position rather than the panel's, so a slide leaves it still pointing at the right thing. It is pure arithmetic with no DOM in it, and exported on its own.
It does not try to be Floating UI. There is no autoUpdate with
ResizeObserver on every ancestor, no virtual elements, no middleware pipeline.
If you need those, use Floating UI — it is the right tool and it is far better
at positioning than this.
What we do not claim
That Floating UI should not be used. The claim is the behaviour layer above it, which it does not attempt.
That the fallback is as good. A browser without the popover API gets a fixed-position panel, which is what every other library does all of the time — so it is no worse than the alternatives, just not as good as the main path.
How it compares
Questions
Should I use this or Floating UI?
Both, or either. Floating UI is a positioning engine and it is better at positioning than this — virtual elements, middleware, auto-updating against every scrolling ancestor. It deliberately stops there. This is the behaviour layer above it: hover intent, touch, focus, ARIA, dismissal, and the WCAG rules about staying open. If you are building something unusual, use Floating UI. If you want a tooltip, this is the tooltip.
What is the top layer, and what happens without it?
It is where the browser paints dialogs and popovers: above every element on the page, outside the normal stacking and clipping rules. It is why there is no z-index here and why an overflow:hidden ancestor does not matter. A browser without it gets a fixed-position panel — which is what every other library does all of the time, so it is no worse than the alternatives, just not as good as the main path.
Is tippy.js really archived?
Yes — the repository is read-only, and it still takes 4.59 million downloads a week. Its most-reacted open issue is #1071, the migration from Popper to Floating UI; #1183 is React 19 support; #1124 is ESM under Vitest. None of them can be merged. It was a good library and this is not a criticism of it.
Why not just use the title attribute?
Where a plain label is all you need, do — it is the most accessible thing available and costs nothing. Reach for this when it has to look like your product, hold markup, stay open long enough to read, or be dismissible with Escape, none of which title can do.
Does it trap focus?
Only when you ask, and only for popovers. focusTrap moves focus into the panel when it opens and back to the anchor when it closes. A tooltip never takes focus, because taking focus away from something you are only describing is how a keyboard user loses their place.
More from Devix
All resources →Toast
UI component
Promise-aware toasts with stacking, swipe to dismiss and correct live regions.
Confirm Dialog
UI component
Promise-based dialogs on native <dialog>: confirm, prompt and type-to-confirm.
Command Palette
UI component
A framework-free ⌘K palette with nested pages, async sources and shortcuts.