
How to add hotspots to your React app
You shipped a new feature and nobody noticed. The button sits there, perfectly styled, completely ignored. This is the exact problem hotspots solve — those small pulsing dots that draw attention to specific UI elements without blocking the user's workflow.
Most React hotspot implementations get the visual part right and the accessibility part wrong. Tooltips that can't be dismissed via keyboard. Beacon animations that ignore prefers-reduced-motion. Hover content that vanishes before you can read it. WCAG 1.4.13 requires hover/focus content to be dismissable, hoverable, and persistent (WCAG Authors Guide), and most tutorials skip all three.
We tested a dozen hotspot approaches while building Tour Kit, and the gotcha that kept coming up was tooltip positioning on scroll. Tour Kit's @tour-kit/hints package gives you hotspot components that handle positioning, accessibility, and dismissal state without fighting your design system. You control the rendering. By the end of this tutorial, you'll have working hotspots attached to real UI elements in a React + TypeScript project.
One honest caveat: Tour Kit requires React 18+ and doesn't have a visual builder. You'll be writing JSX, not dragging and dropping.
npm install @tour-kit/core @tour-kit/hintsWhat you'll build
A React hotspot component is a small visual indicator, typically a pulsing dot, anchored to a specific element in your UI. When a user clicks or focuses the hotspot, a tooltip appears with contextual information about that feature. Tour Kit's <Hint> component handles element tracking, tooltip positioning via Floating UI, keyboard dismissal, and state management through a reducer-based context. You'll build three hotspots attached to different dashboard elements, each with independent open/dismiss state and WCAG-compliant keyboard navigation.
The full example runs in about 40 lines of application code. The library handles the hard parts.
Prerequisites
- React 18.2+ or React 19
- TypeScript 5.0+
- A working React project (Vite, Next.js, or Create React App all work)
- Familiarity with React hooks and context
Step 1: install Tour Kit hints
Tour Kit splits its functionality across focused packages so you only ship what you use. For hotspots, you need two: @tour-kit/core provides the positioning engine and shared types, while @tour-kit/hints adds the hotspot components and state management layer on top.
# npm
npm install @tour-kit/core @tour-kit/hints
# pnpm
pnpm add @tour-kit/core @tour-kit/hints
# yarn
yarn add @tour-kit/core @tour-kit/hintsBoth packages together add under 10KB gzipped to your bundle. They tree-shake cleanly, so if you only use the <Hint> component, unused exports like useHints() get eliminated at build time.
Step 2: wrap your app with HintsProvider
Every hotspot needs access to shared state so that opening one tooltip automatically closes any other. The HintsProvider component manages this through a React reducer, tracking registered hints, open state, and permanent dismissals.
Each <Hint> registers itself on mount and unregisters on unmount, so the provider always knows what's active.
// src/App.tsx
import { HintsProvider } from '@tour-kit/hints'
export function App() {
return (
<HintsProvider>
<Dashboard />
</HintsProvider>
)
}Place HintsProvider high enough in your tree that it wraps every component containing a hotspot. If you're using Next.js App Router, add it to your root layout as a client component.
Step 3: add your first hotspot
Pass the <Hint> component a CSS selector or React ref pointing to your target element, and it handles everything else: tracking element position as the page reflows, rendering the pulsing dot at the correct offset, showing the tooltip on click, and closing it on Escape or outside click. We measured the initial render overhead at under 0.5ms per hotspot in our test app.
// src/components/Dashboard.tsx
import { Hint } from '@tour-kit/hints'
export function Dashboard() {
return (
<div>
<button id="export-btn">Export Data</button>
<button id="filter-btn">Filters</button>
<section id="chart-panel">
<h2>Revenue chart</h2>
{/* ... chart content ... */}
</section>
{/* Hotspot targeting the export button */}
<Hint
id="export-hint"
target="#export-btn"
content="New: export your data as CSV or PDF with one click."
position="top-right"
tooltipPlacement="bottom"
/>
</div>
)
}Selectors like "#export-btn" or ".my-class" work, and so do React refs (more on that in Step 5). Tour Kit watches for the element's position and updates the hotspot if the layout shifts.
Under the hood, the hotspot renders as a <button> with aria-expanded to indicate tooltip state and aria-label="Show hint" for screen readers. When open, the tooltip portals to the document body via Floating UI and positions itself relative to the hotspot button.
Step 4: add multiple hotspots with different configurations
Multiple hotspots can coexist on the same page, each with its own position, color variant, and tooltip placement. The HintsProvider tracks all registered hints and enforces a single-open constraint: clicking one hotspot automatically closes any other open tooltip, so users aren't overwhelmed by overlapping popups.
// src/components/Dashboard.tsx
import { Hint } from '@tour-kit/hints'
export function Dashboard() {
return (
<div>
<button id="export-btn">Export Data</button>
<button id="filter-btn">Filters</button>
<section id="chart-panel">
<h2>Revenue chart</h2>
</section>
<Hint
id="export-hint"
target="#export-btn"
content="New: export your data as CSV or PDF."
position="top-right"
tooltipPlacement="bottom"
color="primary"
size="md"
/>
<Hint
id="filter-hint"
target="#filter-btn"
content="Try the new date range filter."
position="top-left"
tooltipPlacement="right"
color="secondary"
/>
<Hint
id="chart-hint"
target="#chart-panel"
content="Hover over bars to see daily breakdown."
position="center"
tooltipPlacement="top"
persist={true}
/>
</div>
)
}Setting persist={true} on the chart hint means clicking the close button permanently dismisses it (stored as isDismissed: true). Without persist, closing the tooltip just hides it temporarily and the pulsing dot stays visible for reopening.
Position options: top-left, top-right, bottom-left, bottom-right, center. Tooltip placement supports all 12 Floating UI positions (top, bottom, left, right, plus -start and -end variants).
Step 5: use refs for dynamically rendered elements
CSS selectors work for elements that exist in the DOM at render time, but you'll often need to attach hotspots to elements inside modals, lazy-loaded components, or conditional renders. In those cases, pass a React ref to the target prop instead. Tour Kit's useElementPosition hook watches the ref and renders the hotspot only after the element mounts.
// src/components/FeaturePanel.tsx
import { useRef } from 'react'
import { Hint } from '@tour-kit/hints'
export function FeaturePanel() {
const chartRef = useRef<HTMLDivElement>(null)
return (
<div>
<div ref={chartRef} className="chart-container">
{/* Dynamically loaded chart */}
</div>
<Hint
id="chart-interaction-hint"
target={chartRef}
content="Drag to zoom into a specific time range."
position="bottom-right"
tooltipPlacement="left"
/>
</div>
)
}If the target element isn't in the DOM yet, the hotspot simply doesn't render. No errors, no flash of misplaced content. Once the element appears, useElementPosition picks it up and recalculates on scroll, resize, and layout shifts.
Step 6: control hotspots programmatically with useHint
Sometimes you need to show a hotspot after a user completes an action, dismiss all hints when onboarding finishes, or wire hint events into your analytics pipeline. The useHint hook gives you direct control over individual hint state, while useHints exposes bulk operations across all registered hints.
// src/components/OnboardingControls.tsx
import { useHint, useHints } from '@tour-kit/hints'
export function OnboardingControls() {
const exportHint = useHint('export-hint')
const { resetAllHints } = useHints()
return (
<div>
<p>
Export hint: {exportHint.isOpen ? 'open' : 'closed'}
{exportHint.isDismissed && ' (dismissed)'}
</p>
<button onClick={() => exportHint.show()}>
Show export hint
</button>
<button onClick={() => exportHint.dismiss()}>
Dismiss permanently
</button>
<button onClick={() => resetAllHints()}>
Reset all hints
</button>
</div>
)
}useHint(id) returns { isOpen, isDismissed, show, hide, dismiss, reset }. All callbacks are memoized with useCallback, so they're safe to include in dependency arrays.
useHints() gives you access to the full hints map and bulk operations like resetAllHints(). Useful for admin panels or debug tools.
Common issues and troubleshooting
When we built the hints package, these were the three problems that came up most often during testing. Each has a straightforward fix once you know what to look for, and none require changes to Tour Kit's configuration.
"Hotspot doesn't appear on the page"
Check that your target selector matches an element that exists when the <Hint> mounts. If the element renders later (inside a lazy-loaded route, for example), use a ref instead of a selector. Tour Kit waits for the element, but if it never appears, the hotspot stays hidden.
Also verify HintsProvider wraps the component tree containing your hints. Without the provider, useHint throws a context error.
"Tooltip appears in the wrong position"
Floating UI recalculates position on scroll and resize, but if your target element moves due to an animation or CSS transition, the tooltip may lag behind. Use autoShow={false} and trigger the tooltip after animations complete.
For elements inside scrollable containers, confirm that the container has position: relative or overflow: visible. Floating UI's shift() middleware keeps tooltips within viewport bounds, but clipped overflow can still hide them visually.
"Pulse animation doesn't respect reduced motion"
Tour Kit's hotspot variants include a prefers-reduced-motion media query that disables the CSS pulse animation automatically. If you're using a custom className that overrides the animation, add your own motion check:
@media (prefers-reduced-motion: reduce) {
.my-custom-hotspot {
animation: none;
}
}Hotspot approaches compared
Three main approaches exist for adding hotspots to a React application: a dedicated hotspot library like Tour Kit Hints, a full product tour library like React Joyride (which includes beacons as a side feature), or a custom CSS-only implementation that handles the visual indicator but requires you to build all interactivity from scratch.
| Feature | Tour Kit Hints | React Joyride Beacon | Custom CSS-only |
|---|---|---|---|
| Bundle size (gzipped) | <10KB (core + hints) | ~37KB (full library) | 0KB (CSS only) |
| WCAG 1.4.13 compliant | Yes (dismiss, hover, persist) | Partial (no keyboard dismiss on beacon) | No (requires JS) |
| Tooltip positioning | Floating UI (flip, shift, offset) | react-floater (built on Popper.js) | Manual CSS |
| Independent state per hotspot | Yes (reducer-based context) | No (tour-step model) | Manual implementation |
| Headless / unstyled option | Yes (asChild prop + variants) | Limited (custom tooltip component) | N/A |
| prefers-reduced-motion | Built-in | Not built-in | Manual media query |
| Best for | Feature discovery, contextual help | Sequential product tours | Simple visual indicators |
React Joyride's beacon is designed for sequential tours, not standalone hotspots. If you need independent feature hints that persist across sessions and operate outside a tour flow, a dedicated hotspot component is the better fit. Sarah Higley's research on WCAG 1.4.13 (sarahmhigley.com) highlights that hover/focus content must be dismissable via Escape without moving the pointer, a requirement that CSS-only solutions can't meet.
Next steps
With hotspots rendering, positioned correctly, and accessible via keyboard, you have a solid foundation for feature discovery in your React app. Here are four natural extensions that build on what you've already set up.
- Connect
HintsProviderto localStorage or your backend so dismissed hints stay dismissed across sessions. Tour Kit's core package includes storage adapter patterns for this. - Use
@tour-kit/reactfor sequential onboarding alongside@tour-kit/hintsfor persistent feature discovery. Both packages share@tour-kit/core, so they compose without duplication. - Wire
onShowandonDismisscallbacks to your analytics. Which hotspots do users open? Which get dismissed immediately? That data tells you whether your feature discovery is working. - For complete rendering control, import from
@tour-kit/hints/headlessand build your own hotspot UI using theuseHinthook directly.
Tour Kit on GitHub | Hints documentation
FAQ
What is a hotspot component in React?
A React hotspot component is a small visual indicator (usually a pulsing dot) that attaches to a specific element in your UI. Clicking or focusing it reveals a tooltip with contextual help. Tour Kit's <Hint> component provides this pattern with built-in accessibility, Floating UI positioning, and independent state per hotspot.
Does adding hotspots affect React app performance?
Tour Kit's hints package adds under 10KB gzipped to your bundle. The hotspot uses position: fixed with pre-calculated coordinates, so it doesn't trigger layout recalculations. Floating UI's autoUpdate listener only runs when a tooltip is actually open. For most apps, the performance impact is negligible.
How do I make React hotspots accessible?
WCAG 1.4.13 requires hover/focus content to be dismissable (Escape closes it) and persistent (stays visible until user acts). Tour Kit's <Hint> meets both. The hotspot is a focusable <button> with aria-expanded, Escape dismissal works via Floating UI's useDismiss, and the pulse respects prefers-reduced-motion.
How is Tour Kit different from React Joyride for hotspots?
React Joyride's beacon is tied to its sequential tour model. Beacons exist as entry points into tour steps, not as independent elements. Tour Kit's @tour-kit/hints treats each hotspot as a standalone unit with its own state. You can show, hide, dismiss, and reset individual hotspots without affecting others. Joyride ships at roughly 37KB gzipped (LogRocket) versus Tour Kit's under 10KB.
Can I use hotspots on mobile and touch devices?
Tour Kit's hotspots use click events (not hover), which translates directly to tap on touch devices. Sarah Higley's research notes that hover-triggered tooltips are fundamentally broken on mobile because you can't focus a button without activating it (sarahmhigley.com). By using tap as the trigger, Tour Kit sidesteps this entirely.
Related articles

Amplitude + Tour Kit: measuring onboarding impact on retention
Wire Tour Kit callbacks to Amplitude track() for onboarding funnels, behavioral cohorts, and retention analysis. TypeScript examples included.
Read article
How to add a product tour to an Astro site with React islands
Add interactive product tours to an Astro site using React islands. Covers client directives, Nanostores state sharing, and Tour Kit setup.
Read article
Building conditional product tours based on user role
Build role-based product tours in React with Tour Kit. Filter steps by admin, editor, or viewer roles using the when prop and React Context.
Read article
Using CSS container queries for responsive product tours
Build product tour tooltips that adapt to their container, not the viewport. Learn CSS container queries with Tour Kit for truly responsive onboarding.
Read article