@tour-kit/reactHeadless
HeadlessTourOverlay
HeadlessTourOverlay: unstyled overlay primitive with spotlight cutout positioning exposed via render props
domidex01Published
An unstyled overlay that exposes spotlight positioning data via render props.
Usage
import { HeadlessTourOverlay } from '@tour-kit/react/headless';
<HeadlessTourOverlay>
{(props) => (
<div className="my-overlay">
{/* Custom spotlight rendering */}
</div>
)}
</HeadlessTourOverlay>Render Props
Prop
Type
SVG Mask Example
<HeadlessTourOverlay>
{({ isVisible, targetRect, padding, borderRadius }) => {
if (!isVisible || !targetRect) return null;
return (
<svg className="fixed inset-0 w-full h-full z-40 pointer-events-none">
<defs>
<mask id="spotlight-mask">
<rect width="100%" height="100%" fill="white" />
<rect
x={targetRect.left - padding}
y={targetRect.top - padding}
width={targetRect.width + padding * 2}
height={targetRect.height + padding * 2}
rx={borderRadius}
fill="black"
/>
</mask>
</defs>
<rect
width="100%"
height="100%"
fill="rgba(0, 0, 0, 0.5)"
mask="url(#spotlight-mask)"
/>
</svg>
);
}}
</HeadlessTourOverlay>CSS Box-Shadow Approach
<HeadlessTourOverlay>
{({ isVisible, targetRect, padding }) => {
if (!isVisible || !targetRect) return null;
return (
<div
className="fixed z-40 pointer-events-none"
style={{
top: targetRect.top - padding,
left: targetRect.left - padding,
width: targetRect.width + padding * 2,
height: targetRect.height + padding * 2,
borderRadius: 8,
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)',
}}
/>
);
}}
</HeadlessTourOverlay>Animated Overlay
import { motion } from 'framer-motion';
<HeadlessTourOverlay>
{({ isVisible, targetRect, padding, borderRadius }) => {
if (!isVisible || !targetRect) return null;
return (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 bg-black/50 z-40"
/>
{/* Spotlight cutout */}
<motion.div
initial={{ scale: 0.8, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
className="fixed z-40 bg-transparent"
style={{
top: targetRect.top - padding,
left: targetRect.left - padding,
width: targetRect.width + padding * 2,
height: targetRect.height + padding * 2,
borderRadius,
boxShadow: '0 0 0 9999px rgba(0, 0, 0, 0.5)',
}}
/>
</>
);
}}
</HeadlessTourOverlay>Click Handling
Add click-through for the target or click-outside handling:
<HeadlessTourOverlay>
{({ isVisible, targetRect, padding }) => {
if (!isVisible) return null;
const handleClickOutside = (e: React.MouseEvent) => {
if (!targetRect) return;
const { clientX, clientY } = e;
const inSpotlight =
clientX >= targetRect.left - padding &&
clientX <= targetRect.right + padding &&
clientY >= targetRect.top - padding &&
clientY <= targetRect.bottom + padding;
if (!inSpotlight) {
// Handle click outside spotlight
console.log('Clicked outside');
}
};
return (
<div
className="fixed inset-0 z-40"
onClick={handleClickOutside}
>
{/* Your overlay rendering */}
</div>
);
}}
</HeadlessTourOverlay>Canvas Rendering
For complex effects:
<HeadlessTourOverlay>
{({ isVisible, targetRect, padding, borderRadius }) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
if (!isVisible || !targetRect || !canvasRef.current) return;
const ctx = canvasRef.current.getContext('2d');
if (!ctx) return;
// Clear and draw overlay
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(0, 0, window.innerWidth, window.innerHeight);
// Cut out spotlight
ctx.globalCompositeOperation = 'destination-out';
ctx.beginPath();
ctx.roundRect(
targetRect.left - padding,
targetRect.top - padding,
targetRect.width + padding * 2,
targetRect.height + padding * 2,
borderRadius
);
ctx.fill();
}, [isVisible, targetRect, padding, borderRadius]);
return (
<canvas
ref={canvasRef}
width={window.innerWidth}
height={window.innerHeight}
className="fixed inset-0 z-40 pointer-events-none"
/>
);
}}
</HeadlessTourOverlay>