CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-popperjs--core

Tooltip and popover positioning engine that automatically calculates optimal placement for UI elements with advanced positioning logic and overflow prevention

Pending

Quality

Pending

Does it follow best practices?

Impact

Pending

No eval scenarios have been run

Overview
Eval results
Files

virtual-elements.mddocs/

Virtual Elements

Support for positioning relative to virtual coordinates for context menus, mouse-following tooltips, and positioning relative to abstract points rather than DOM elements.

Capabilities

Virtual Element Interface

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();
});

Mouse-Following Tooltips

Create tooltips that follow the mouse cursor.

// No specific API - use VirtualElement with mouse coordinates

Usage 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';
});

Context Menus

Position context menus relative to click coordinates.

// No specific API - use VirtualElement with click coordinates

Usage 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);
});

Selection-Based Positioning

Position tooltips relative to text selections or ranges.

// No specific API - use VirtualElement with selection coordinates

Usage 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;
    }
  }
});

Dynamic Virtual Elements

Virtual elements that change position or size over time.

// No specific API - implement getBoundingClientRect with dynamic values

Usage 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();

Context Elements

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-container

Error Handling

Common 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

docs

built-in-modifiers.md

core-positioning.md

index.md

variants-tree-shaking.md

virtual-elements.md

tile.json