@tour-kit/coreHooks
useElementPosition
useElementPosition hook: track DOM element position with automatic updates on scroll, resize, and layout changes
domidex01Published
Track a DOM element's position in real-time. Automatically updates when the element or window scrolls, resizes, or when the element's size changes.
Why Use This Hook?
Tour overlays and tooltips need to stay aligned with their target elements. This hook:
- Tracks position changes - Updates when elements move due to scroll or resize
- Uses ResizeObserver - Detects when elements change size
- Finds scroll parents - Handles nested scrollable containers
- Handles dynamic targets - Accepts CSS selectors or element refs
Usage
import { useElementPosition } from '@tour-kit/core';
function SpotlightOverlay() {
const { element, rect, update } = useElementPosition('#my-target');
if (!rect) return null;
return (
<div
style={{
position: 'fixed',
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height,
border: '2px solid blue',
pointerEvents: 'none',
}}
/>
);
}Parameters
Prop
Type
Return Value
Prop
Type
With CSS Selector
function TargetHighlight() {
const { rect } = useElementPosition('#signup-button');
if (!rect) {
return <p>Element not found</p>;
}
return (
<div
className="fixed bg-blue-500/20 rounded pointer-events-none"
style={{
top: rect.top - 4,
left: rect.left - 4,
width: rect.width + 8,
height: rect.height + 8,
}}
/>
);
}With Element Ref
import { useRef } from 'react';
import { useElementPosition } from '@tour-kit/core';
function TrackedElement() {
const buttonRef = useRef<HTMLButtonElement>(null);
const { rect } = useElementPosition(buttonRef.current);
return (
<>
<button ref={buttonRef}>Track me</button>
{rect && (
<span className="text-sm text-gray-500">
Position: {Math.round(rect.top)}, {Math.round(rect.left)}
</span>
)}
</>
);
}Manual Updates
For cases where you know the element moved but the automatic detection missed it:
function AnimatedElement() {
const { rect, update } = useElementPosition('#animated-box');
const handleAnimationEnd = () => {
// Force position recalculation after animation
update();
};
return (
<div id="animated-box" onAnimationEnd={handleAnimationEnd}>
Animated content
</div>
);
}Nested Scroll Containers
The hook automatically detects nested scrollable parents:
function NestedScrollExample() {
const { rect, scrollParent } = useElementPosition('#nested-target');
// scrollParent will be the nearest scrollable container,
// not necessarily the window
return (
<div className="h-64 overflow-auto">
<div className="h-[200vh]">
<button id="nested-target">Target in scrollable container</button>
</div>
</div>
);
}How It Works
- Element Resolution - Converts CSS selector to element, or uses the provided element directly
- Scroll Parent Detection - Finds the nearest ancestor with
overflow: auto/scroll - Event Listeners - Attaches scroll and resize listeners to window (with capture)
- ResizeObserver - Observes both the target and its scroll parent for size changes
- Cleanup - Removes all listeners and observers when unmounting or target changes
The hook uses getBoundingClientRect() for position calculations, which returns coordinates relative to the viewport.
Performance Notes
- Position updates are synchronous for immediate visual feedback
- ResizeObserver is more efficient than polling
- Scroll events use capture phase to catch all scroll events in the hierarchy
Related
- useSpotlight - Uses element position for overlay calculations
- TourOverlay - Component that uses this hook internally