Tooltip and popover positioning engine that automatically calculates optimal placement for UI elements with advanced positioning logic and overflow prevention
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Support for positioning relative to virtual coordinates for context menus, mouse-following tooltips, and positioning relative to abstract points rather than DOM elements.
Definition for objects that can be used as reference elements without being actual DOM elements.
/**
* Virtual element that can be positioned relative to
* Must implement getBoundingClientRect to define positioning reference
*/
interface VirtualElement {
/** Returns rectangle defining the virtual element's position and size */
getBoundingClientRect(): ClientRect | DOMRect;
/** Optional context element for boundary calculations */
contextElement?: Element;
}
interface ClientRect {
x: number;
y: number;
top: number;
left: number;
right: number;
bottom: number;
width: number;
height: number;
}Usage Examples:
import { createPopper } from '@popperjs/core';
// Mouse cursor virtual element
const cursorVirtualElement = {
getBoundingClientRect() {
return {
x: mouseX,
y: mouseY,
top: mouseY,
left: mouseX,
right: mouseX,
bottom: mouseY,
width: 0,
height: 0,
};
},
};
const popper = createPopper(cursorVirtualElement, tooltip, {
placement: 'bottom-start',
});
// Update position on mouse move
document.addEventListener('mousemove', (event) => {
mouseX = event.clientX;
mouseY = event.clientY;
popper.update();
});Create tooltips that follow the mouse cursor.
// No specific API - use VirtualElement with mouse coordinatesUsage Examples:
import { createPopper } from '@popperjs/core';
let mouseX = 0;
let mouseY = 0;
// Virtual element that tracks mouse position
const mouseVirtualElement = {
getBoundingClientRect() {
return {
x: mouseX,
y: mouseY,
top: mouseY,
left: mouseX,
right: mouseX,
bottom: mouseY,
width: 0,
height: 0,
};
},
};
// Create popper with virtual element
const mousePopper = createPopper(mouseVirtualElement, tooltip, {
placement: 'bottom-start',
modifiers: [
{
name: 'offset',
options: {
offset: [10, 10], // Offset from cursor
},
},
],
});
// Track mouse movement
document.addEventListener('mousemove', (event) => {
mouseX = event.clientX;
mouseY = event.clientY;
// Update popper position
mousePopper.update();
});
// Show tooltip on hover
document.addEventListener('mouseenter', () => {
tooltip.style.visibility = 'visible';
});
document.addEventListener('mouseleave', () => {
tooltip.style.visibility = 'hidden';
});Position context menus relative to click coordinates.
// No specific API - use VirtualElement with click coordinatesUsage Examples:
import { createPopper } from '@popperjs/core';
// Context menu virtual element
function createContextMenuVirtualElement(x, y) {
return {
getBoundingClientRect() {
return {
x,
y,
top: y,
left: x,
right: x,
bottom: y,
width: 0,
height: 0,
};
},
};
}
// Show context menu on right-click
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
const virtualElement = createContextMenuVirtualElement(
event.clientX,
event.clientY
);
const contextPopper = createPopper(virtualElement, contextMenu, {
placement: 'bottom-start',
modifiers: [
{
name: 'flip',
options: {
boundary: 'viewport',
},
},
{
name: 'preventOverflow',
options: {
boundary: 'viewport',
},
},
],
});
contextMenu.style.visibility = 'visible';
// Hide on click outside
const hideMenu = () => {
contextMenu.style.visibility = 'hidden';
contextPopper.destroy();
document.removeEventListener('click', hideMenu);
};
document.addEventListener('click', hideMenu);
});Position tooltips relative to text selections or ranges.
// No specific API - use VirtualElement with selection coordinatesUsage Examples:
import { createPopper } from '@popperjs/core';
// Create virtual element from text selection
function createSelectionVirtualElement() {
const selection = window.getSelection();
if (!selection.rangeCount) return null;
const range = selection.getRangeAt(0);
const rect = range.getBoundingClientRect();
return {
getBoundingClientRect() {
return rect;
},
contextElement: range.commonAncestorContainer.parentElement,
};
}
// Show tooltip for selected text
document.addEventListener('selectionchange', () => {
const selection = window.getSelection();
if (selection.toString().length > 0) {
const virtualElement = createSelectionVirtualElement();
if (virtualElement) {
const selectionPopper = createPopper(virtualElement, selectionTooltip, {
placement: 'top',
modifiers: [
{
name: 'offset',
options: {
offset: [0, 8],
},
},
],
});
selectionTooltip.style.visibility = 'visible';
// Store reference for cleanup
window.currentSelectionPopper = selectionPopper;
}
} else {
// Hide tooltip when selection is cleared
if (window.currentSelectionPopper) {
selectionTooltip.style.visibility = 'hidden';
window.currentSelectionPopper.destroy();
window.currentSelectionPopper = null;
}
}
});Virtual elements that change position or size over time.
// No specific API - implement getBoundingClientRect with dynamic valuesUsage Examples:
import { createPopper } from '@popperjs/core';
// Animated virtual element
class AnimatedVirtualElement {
constructor(startX, startY, endX, endY, duration) {
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
this.duration = duration;
this.startTime = Date.now();
}
getBoundingClientRect() {
const elapsed = Date.now() - this.startTime;
const progress = Math.min(elapsed / this.duration, 1);
// Easing function
const eased = 1 - Math.pow(1 - progress, 3);
const x = this.startX + (this.endX - this.startX) * eased;
const y = this.startY + (this.endY - this.startY) * eased;
return {
x,
y,
top: y,
left: x,
right: x,
bottom: y,
width: 0,
height: 0,
};
}
}
// Create animated tooltip
const animatedVirtual = new AnimatedVirtualElement(100, 100, 300, 200, 1000);
const animatedPopper = createPopper(animatedVirtual, tooltip, {
placement: 'top',
});
// Update during animation
const animate = () => {
animatedPopper.update();
const elapsed = Date.now() - animatedVirtual.startTime;
if (elapsed < animatedVirtual.duration) {
requestAnimationFrame(animate);
}
};
animate();Using contextElement for proper boundary calculations with virtual elements.
interface VirtualElement {
getBoundingClientRect(): ClientRect | DOMRect;
/** Element to use for scroll parent and boundary calculations */
contextElement?: Element;
}Usage Examples:
import { createPopper } from '@popperjs/core';
// Virtual element with context for proper scrolling behavior
const virtualWithContext = {
getBoundingClientRect() {
return {
x: 150,
y: 150,
top: 150,
left: 150,
right: 150,
bottom: 150,
width: 0,
height: 0,
};
},
// Use container for scroll parent detection
contextElement: document.querySelector('.scrollable-container'),
};
const popper = createPopper(virtualWithContext, tooltip, {
placement: 'bottom',
modifiers: [
{
name: 'preventOverflow',
options: {
boundary: 'clippingParents', // Will use contextElement's clipping parents
},
},
],
});
// The popper will now properly handle scrolling within .scrollable-containerCommon patterns for handling virtual element edge cases.
// Validate virtual element before use
function createSafeVirtualElement(x, y, contextElement) {
// Ensure coordinates are valid numbers
if (typeof x !== 'number' || typeof y !== 'number' ||
!isFinite(x) || !isFinite(y)) {
throw new Error('Invalid coordinates for virtual element');
}
return {
getBoundingClientRect() {
return {
x,
y,
top: y,
left: x,
right: x,
bottom: y,
width: 0,
height: 0,
};
},
contextElement,
};
}
// Handle dynamic coordinate updates safely
function updateVirtualElementPopper(popper, x, y) {
if (popper && typeof x === 'number' && typeof y === 'number') {
// Update the virtual element's coordinates
// Then update popper position
popper.update();
}
}
// Clean up event listeners when destroying virtual element poppers
function cleanupVirtualPopper(popper) {
if (popper) {
popper.destroy();
// Remove any associated event listeners
document.removeEventListener('mousemove', updateHandler);
document.removeEventListener('click', clickHandler);
}
}Install with Tessl CLI
npx tessl i tessl/npm-popperjs--core