Skip to content
react-headless-tour
⚡ Just 4.5 kB — the lightest fully-featured React tour library

Product tours that wear
your design system

All the logic — targeting, positioning, animated spotlight, keyboard navigation — none of the opinions. Restyle it with CSS variables, class names, or replace every component with your own.

4.5 kB core, min+gzip1 dependency0 animation libraries100% yours to style

Live playground

Revenue
$48,210
+12.4%
Active users
8,431
+3.1%
Churn
1.9%
-0.4%
clickable during the tour

Why another tour library?

Most tour packages ship their own look and fight your theme. This one is headless: it does the hard parts and stays out of your stylesheet.

Three levels of theming

CSS variables → class names → replace whole components. Your design system wins.

Springy spotlight

An SVG-mask cutout glides smoothly between targets — pure CSS, no animation library.

Bulletproof positioning

Floating UI under the hood — auto flip, shift and arrow. No clipped tooltips.

Keyboard navigation

Arrow keys, Enter and Escape work out of the box. Fully optional.

Interactable steps

Let users click the highlighted element mid-tour — great for guided actions.

Follows moving targets

Survives scrolling, resizes, sticky headers and layout shifts via live tracking.

Next.js ready

SSR-safe, ships "use client" for the App Router. ESM + CJS + full types.

Tiny

~4.5 kB min+gzip of library code. One provider, one hook, one dependency.

Size matters

Every kilobyte you ship is loading time your users pay for a feature they see once.

LibraryBundle cost (min+gzip)React componentsThemingLicense
react-headless-tour12.3 kB (4.5 kB core)✅ React✅ Fully headlessMIT
react-joyride25.0 kB✅ React⚠️ Styled, override via propsMIT
intro.js17.2 kB❌ Vanilla + wrapper⚠️ Theme via CSS overridesAGPL / paid commercial
driver.js7.2 kB❌ Vanilla⚠️ Theme via CSS overridesMIT

Sizes measured via bundlephobia, September 2026. “Core” excludes the Floating UI positioning engine (shared with many popover/tooltip libraries, so you may already ship it).

Documentation

The essentials — the full API reference lives in the README.

1 · Quick start

import { TourProvider, useTour, type TourStep } from "react-headless-tour";

const steps: TourStep[] = [
  { title: "Welcome 👋", content: "No target = centered modal step." },
  { target: "#stats", title: "Your metrics", content: "At a glance." },
  { target: "#new", title: "Create one", placement: "left", interactable: true },
];

function StartButton() {
  const { start } = useTour();
  return <button onClick={() => start()}>Start tour</button>;
}

export default function App() {
  return (
    <TourProvider steps={steps}>
      <StartButton />
      {/* ...your app... */}
    </TourProvider>
  );
}

2 · Theme with CSS variables (zero code)

Every visual decision of the default card is a --tour-* variable with a sensible fallback. Scope them under a class and pass it as classNames={{ root: "my-tour" }}, or inline via the theme prop:

.my-tour {
  --tour-bg: #16181d;          /* card background   */
  --tour-fg: #f9fafb;          /* card text         */
  --tour-accent: #4f46e5;      /* primary button    */
  --tour-radius: 14px;
  --tour-overlay-color: rgba(10, 12, 24, 0.68);
}

3 · Or bring your own components (fully headless)

Replace the card entirely with components={{ Card }}. You receive the whole tour state and controls as typed props — the library keeps handling positioning, the spotlight and animations. The gradient “Brand” flavor in the playground above is exactly this.

import type { TourCardProps } from "react-headless-tour";

function MyCard({ step, next, prev, stop, isLast, arrow }: TourCardProps) {
  return (
    <div className="my-design-system-popover">
      {arrow /* optional — skip it for an arrowless design */}
      <h3>{step.title}</h3>
      <p>{step.content}</p>
      <button onClick={next}>{isLast ? "Done" : "Next"}</button>
    </div>
  );
}

<TourProvider steps={steps} components={{ Card: MyCard }} />

API reference

Everything below is fully typed — your editor shows these same descriptions via IntelliSense.

<TourProvider> props

PropTypeDefaultDescription
stepsTourStep[]—The tour definition. Required.
autoStartbooleanfalseStart the tour automatically on mount.
stepIndexnumber—Controlled mode: you own the active step index. Pair with onStepChange.
onStepChange(index, step) => void—Fires whenever the active step changes (buttons, keyboard or goTo).
onStart() => void—Fires when the tour starts.
onStop(reason, lastIndex) => void—Fires when the tour ends. reason: "finished" | "skipped" | "escape" | "mask" | "programmatic".
components{ Card?, Arrow? }—Replace the card and/or arrow with your own components (fully headless).
classNamesTourClassNames—Class hooks for every rendered part — style with Tailwind, CSS Modules, anything.
theme{ "--tour-*": value }—Inline CSS-variable overrides, applied on the tour root so they always win.
overlayColorstringrgba(0,0,0,0.55)Dimming color. Also themeable via --tour-overlay-color.
overlayBlurnumber0Frosted-glass blur behind the overlay, in px.
showOverlaybooleantrueRender the dimming overlay at all.
closeOnMaskClickbooleanfalseClicking the dimmed area stops the tour.
keyboardbooleantrue← → navigate, Enter advances, Esc stops.
spotlightPaddingnumber8Space between the target and the spotlight edge, in px.
spotlightRadiusnumber8Corner radius of the spotlight hole, in px.
offsetnumber12Gap between the target and the popover, in px.
lockScrollbooleanfalseLock body scroll while the tour is active.
scrollIntoViewOptionsScrollIntoViewOptionssmooth / centerHow targets are auto-scrolled into view.
portalContainerElementdocument.bodyWhere the tour UI is portaled.
zIndexnumber10000z-index of the tour root. Also --tour-z-index.
labels{ next?, prev?, finish?, skip?, progress? }—Texts for the default card — plain strings or nodes, i18n-ready.

TourStep

FieldTypeDefaultDescription
targetstring | () => Element | null—CSS selector or resolver for the element to highlight. Omit for a centered modal step.
titleReactNode—Heading shown by the default card (custom cards receive the whole step).
contentReactNode—Body shown by the default card.
placement"top" | "bottom" | "left" | "right" (+ -start / -end)"bottom"Preferred popover side. Flips and shifts automatically when space runs out.
interactablebooleanfalseLet the user click/type on the highlighted element during the step.
spotlightPaddingnumberprovider valuePer-step override of the spotlight padding.
spotlightRadiusnumberprovider valuePer-step override of the spotlight corner radius.
disableScrollbooleanfalseSkip auto-scrolling the target into view for this step.
dataunknown—Free-form payload passed through to a custom Card.
onEnter / onExit(step, index) => void—Lifecycle hooks — e.g. navigate to another page in onEnter for multi-page tours.

useTour()

Call it anywhere under the provider — ideal for “restart tour” menu items or driving the tour from app logic. Custom Cards receive all of this as props too.

MemberTypeDescription
isActivebooleanWhether the tour is running.
stepIndex / totalStepsnumberActive step index (-1 when off) and total step count.
stepTourStep | nullThe active step object.
isFirst / isLastbooleanPosition helpers for building custom cards.
start(atIndex?)functionStart the tour, optionally at a specific step.
stop(reason?)functionStop the tour.
next() / prev() / goTo(i)functionNavigate between steps.

CSS variables

All accepted by the default card and overlay: --tour-bg, --tour-fg, --tour-muted, --tour-accent, --tour-accent-fg, --tour-radius, --tour-shadow, --tour-border, --tour-font, --tour-max-width, --tour-padding, --tour-arrow-size, --tour-overlay-color, --tour-z-index