Skip to main content
userTourKit
@tour-kit/coreUtilities

Position Utilities

Position engine: calculate tooltip placement with collision detection, viewport clamping, and RTL layout support

domidex01Published

Calculate tooltip and card positions relative to target elements. Includes viewport collision detection and RTL layout support.

Why Use These Utilities?

Tour cards need to be positioned correctly relative to their targets:

  • Multiple placements - Top, bottom, left, right with alignments
  • Collision detection - Flip when hitting viewport edges
  • RTL support - Mirror placements for right-to-left layouts
  • Responsive - Adjust based on available space

calculatePosition

Calculate the position for a tooltip relative to a target element.

Usage

import { calculatePosition, getElementRect } from '@tour-kit/core';

const target = document.getElementById('button');
const targetRect = getElementRect(target);

const tooltipSize = { width: 300, height: 150 };

const position = calculatePosition(
  targetRect,
  tooltipSize,
  'bottom', // placement
  [0, 8]   // offset [x, y]
);

// position = { x: 150, y: 248 }

Parameters

Prop

Type

Return Value

{ x: number; y: number }

calculatePositionWithCollision

Calculate position with automatic fallback when hitting viewport edges.

Usage

import { calculatePositionWithCollision, getElementRect } from '@tour-kit/core';

const targetRect = getElementRect(document.getElementById('button'));
const tooltipSize = { width: 300, height: 150 };

const result = calculatePositionWithCollision(
  targetRect,
  tooltipSize,
  'top',
  {
    offset: [0, 8],
    padding: 16,
  }
);

// result = { x: 150, y: 50, placement: 'bottom', hasOverflow: false }
// Note: 'top' was requested but 'bottom' was used due to collision

Parameters

Prop

Type

Options

Prop

Type

Return Value

interface PositionResult {
  x: number;
  y: number;
  placement: Placement;   // Actual placement used
  hasOverflow: boolean;   // True if no placement fit
}

Placement Options

// Simple side placements
'top'    // Centered above target
'bottom' // Centered below target
'left'   // Centered to the left
'right'  // Centered to the right
// With alignment
'top-start'    // Above, aligned to start (left in LTR)
'top-end'      // Above, aligned to end (right in LTR)
'bottom-start' // Below, aligned to start
'bottom-end'   // Below, aligned to end
'left-start'   // Left, aligned to top
'left-end'     // Left, aligned to bottom
'right-start'  // Right, aligned to top
'right-end'    // Right, aligned to bottom

RTL Support

getDocumentDirection

Detect the document's text direction.

import { getDocumentDirection } from '@tour-kit/core';

const dir = getDocumentDirection(); // 'ltr' or 'rtl'

mirrorPlacementForRTL

Mirror a placement for RTL layouts.

import { mirrorPlacementForRTL } from '@tour-kit/core';

// In RTL mode:
mirrorPlacementForRTL('left', true);       // 'right'
mirrorPlacementForRTL('right-start', true); // 'left-end'
mirrorPlacementForRTL('top-start', true);   // 'top-end'
mirrorPlacementForRTL('bottom', true);      // 'bottom' (unchanged)

RTL-Aware Positioning

import {
  calculatePositionWithCollision,
  getDocumentDirection,
  mirrorPlacementForRTL,
} from '@tour-kit/core';

function getTooltipPosition(target, size, placement) {
  const isRTL = getDocumentDirection() === 'rtl';
  const effectivePlacement = mirrorPlacementForRTL(placement, isRTL);

  return calculatePositionWithCollision(
    getElementRect(target),
    size,
    effectivePlacement
  );
}

Helper Functions

getElementRect

Get element position including scroll offset.

import { getElementRect } from '@tour-kit/core';

const rect = getElementRect(element);
// { x: 100, y: 200, width: 150, height: 40 }

Unlike getBoundingClientRect(), this includes scroll offset so positions work correctly after scrolling.

getViewportDimensions

Get current viewport size.

import { getViewportDimensions } from '@tour-kit/core';

const viewport = getViewportDimensions();
// { width: 1920, height: 1080 }

parsePlacement

Parse placement string into side and alignment.

import { parsePlacement } from '@tour-kit/core';

parsePlacement('top-start');
// { side: 'top', alignment: 'start' }

parsePlacement('bottom');
// { side: 'bottom', alignment: 'center' }

getOppositeSide

Get the opposite side for fallback positioning.

import { getOppositeSide } from '@tour-kit/core';

getOppositeSide('top');    // 'bottom'
getOppositeSide('left');   // 'right'

getFallbackPlacements

Get fallback placements for collision handling.

import { getFallbackPlacements } from '@tour-kit/core';

getFallbackPlacements('top');
// ['bottom', 'left', 'right']

getFallbackPlacements('top-start');
// ['bottom-start', 'left-start', 'right-start', 'top-end']

wouldOverflow

Check if a position would cause viewport overflow.

import { wouldOverflow, getViewportDimensions } from '@tour-kit/core';

const overflow = wouldOverflow(
  { x: -10, y: 50 },
  { width: 200, height: 100 },
  getViewportDimensions()
);
// { top: false, right: false, bottom: false, left: true }

Complete Positioning Example

import {
  calculatePositionWithCollision,
  getElementRect,
  getDocumentDirection,
  mirrorPlacementForRTL,
} from '@tour-kit/core';

function TourTooltip({ target, placement = 'bottom' }) {
  const tooltipRef = useRef<HTMLDivElement>(null);
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [actualPlacement, setActualPlacement] = useState(placement);

  useEffect(() => {
    if (!tooltipRef.current) return;

    const targetRect = getElementRect(target);
    const tooltipSize = {
      width: tooltipRef.current.offsetWidth,
      height: tooltipRef.current.offsetHeight,
    };

    // Handle RTL
    const isRTL = getDocumentDirection() === 'rtl';
    const effectivePlacement = mirrorPlacementForRTL(placement, isRTL);

    // Calculate with collision detection
    const result = calculatePositionWithCollision(
      targetRect,
      tooltipSize,
      effectivePlacement,
      { offset: [0, 8], padding: 16 }
    );

    setPosition({ x: result.x, y: result.y });
    setActualPlacement(result.placement);
  }, [target, placement]);

  return (
    <div
      ref={tooltipRef}
      style={{
        position: 'absolute',
        left: position.x,
        top: position.y,
      }}
      data-placement={actualPlacement}
    >
      Tooltip content
    </div>
  );
}

Types

type Side = 'top' | 'bottom' | 'left' | 'right';

type Alignment = 'start' | 'center' | 'end';

type Placement =
  | Side
  | `${Side}-start`
  | `${Side}-end`;

interface Rect {
  x: number;
  y: number;
  width: number;
  height: number;
}

interface Position {
  x: number;
  y: number;
}