@tour-kit/hintsHeadless
HintHotspotHeadless
HeadlessHintHotspot: unstyled hotspot trigger with positioning data exposed via render props for custom beacons
HintHotspotHeadless
A headless positioned button that serves as the hint trigger. Calculates position based on target element and provides render props for custom styling.
Why Use This Component?
- Position calculation - Automatically positions relative to target
- Custom rendering - Full control over appearance
- Accessibility - ARIA attributes included
- Ref forwarding - Attach your own ref if needed
Basic Usage
import { HintHotspotHeadless } from '@tour-kit/hints/headless';
function CustomHotspot() {
const targetRect = element.getBoundingClientRect();
return (
<HintHotspotHeadless
targetRect={targetRect}
position="top-right"
isOpen={false}
onClick={() => setIsOpen(true)}
className="w-4 h-4 bg-blue-500 rounded-full"
/>
);
}Props
Prop
Type
Also accepts all <button> props.
Render Props
Prop
Type
With Render Prop
<HintHotspotHeadless
targetRect={targetRect}
position="top-right"
isOpen={isOpen}
render={({ position, isOpen }) => (
<button
onClick={toggleTooltip}
className="fixed z-50"
style={{ top: position.top, left: position.left }}
>
{/* Pulsing dot */}
<span className="flex h-4 w-4">
<span
className={`
absolute h-4 w-4 rounded-full bg-blue-400
${isOpen ? '' : 'animate-ping opacity-75'}
`}
/>
<span className="relative rounded-full h-4 w-4 bg-blue-500" />
</span>
</button>
)}
/>Position Options
type HotspotPosition =
| 'top-left' // Above target, left edge
| 'top-right' // Above target, right edge
| 'bottom-left' // Below target, left edge
| 'bottom-right' // Below target, right edge
| 'center'; // Center of targetPosition Calculation
Each position is offset 4px from the target edge:
switch (position) {
case 'top-left':
return { top: rect.top - 4, left: rect.left - 4 };
case 'top-right':
return { top: rect.top - 4, left: rect.right - 4 };
case 'bottom-left':
return { top: rect.bottom - 4, left: rect.left - 4 };
case 'bottom-right':
return { top: rect.bottom - 4, left: rect.right - 4 };
case 'center':
return {
top: rect.top + rect.height / 2 - 6,
left: rect.left + rect.width / 2 - 6,
};
}Custom Pulsing Animation
<HintHotspotHeadless
targetRect={targetRect}
position="top-right"
render={({ position }) => (
<button
style={{
position: 'fixed',
top: position.top,
left: position.left,
}}
className="relative"
>
{/* Outer pulse ring */}
<span className="absolute inset-0 animate-ping rounded-full bg-green-400 opacity-75" />
{/* Inner dot */}
<span className="relative block h-3 w-3 rounded-full bg-green-500" />
</button>
)}
/>With Icon
<HintHotspotHeadless
targetRect={targetRect}
position="bottom-right"
render={({ position }) => (
<button
style={{
position: 'fixed',
top: position.top,
left: position.left,
}}
className="p-1 bg-blue-500 text-white rounded-full hover:bg-blue-600"
>
<InfoIcon className="w-4 h-4" />
</button>
)}
/>Accessibility
The component includes:
type="button"to prevent form submissionaria-label="Show hint"for screen readersaria-expanded={isOpen}to indicate state
// Default rendering includes:
<button
type="button"
aria-label="Show hint"
aria-expanded={isOpen}
/>Ref Forwarding
const hotspotRef = useRef<HTMLButtonElement>(null);
<HintHotspotHeadless
ref={hotspotRef}
targetRect={targetRect}
position="top-right"
/>
// hotspotRef.current is the button elementRelated
- HintHeadless - Parent container
- HintTooltipHeadless - Tooltip component
- Hint Components - Styled alternatives