TourKit
@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 target

Position 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 submission
  • aria-label="Show hint" for screen readers
  • aria-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 element

On this page