Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@echoeyecodes/simple-tour

A small headless React library for guided product tours. Steps register from the React tree (no DOM selectors), the library handles positioning and lifecycle, you bring the styling.

Install

npm install @echoeyecodes/simple-tour @radix-ui/react-slot

react, react-dom, and @radix-ui/react-slot are peer dependencies.

Usage

import {
  TourProvider,
  TourPortal,
  TourHighlight,
  TourContent,
  TourTrigger,
  TourNext,
  TourPrevious,
  TourSkip,
  useTourStep,
  useTourActive,
} from "@echoeyecodes/simple-tour";

function Page() {
  const buttonRef = useTourStep({
    id: "publish",
    order: 1,
    placement: "bottom",
    content: (
      <div>
        <h3>Publish</h3>
        <p>When everything looks right, hit Publish.</p>
      </div>
    ),
  });

  return (
    <div>
      <TourTrigger>Take a tour</TourTrigger>
      <button ref={buttonRef}>Publish</button>
    </div>
  );
}

export function App() {
  return (
    <TourProvider>
      <Page />

      <TourPortal>
        <TourHighlight className="[--tour-pad:8px] ..." />
        <TourContent className="...">
          <ActiveStepContent />
          <div>
            <TourSkip>Skip</TourSkip>
            <TourPrevious>Back</TourPrevious>
            <TourNext>Next</TourNext>
          </div>
        </TourContent>
      </TourPortal>
    </TourProvider>
  );
}

function ActiveStepContent() {
  const { step } = useTourActive();
  return <>{step.contentRef.current}</>;
}

Concepts

Steps register from the tree

useTourStep({ id, order, placement, content }) returns a RefCallback<HTMLElement>. Attach it to whatever element should be highlighted. The step is registered on mount and removed on unmount, so steps living inside conditionally rendered components — modals, tabs, route children — are handled naturally.

TourPortal is the active-tour boundary

TourPortal only mounts its children when (a) a tour is active, (b) the active step is registered, and (c) its target element has a measurable rect. Anything inside TourPortal can use useTourActive() to read the current step and target rect — no nullable state to handle.

Primitives are unstyled

TourHighlight, TourContent, TourSkip, TourPrevious, TourNext, TourTrigger ship with no visual styling. They expose className, style, and asChild (via @radix-ui/react-slot). Compose them with your own components.

Primitive What it does
TourProvider State + context. Wrap your app (or part of it).
TourPortal Renders children to document.body. Only when active. Handles scroll-into-view.
TourHighlight A positioned element matching the target rect. Set --tour-pad to expand it outward (e.g. className="[--tour-pad:8px]").
TourContent A positioned tooltip near the target. Auto-flips placement on overflow.
TourTrigger A button that calls start().
TourNext / TourPrevious / TourSkip Buttons wired to the corresponding control.

Hooks

useTour();           // { active, currentStepIndex, totalSteps, isFirst, isLast, start, next, previous, end, goTo }
useTourActive();     // { step, rect } — only valid inside <TourPortal>
useTourStep({ ... }) // ref callback to attach to the target element

Asynchronous targets

If you call next() while the next step's element isn't mounted yet (e.g. it lives on a different route), the tour pauses. Once the element registers, the tooltip appears. Nothing to wire up.

Build

bun install
bun run build

Outputs ESM + CJS + types into dist/.

License

MIT

About

A tiny headless React library for guided product tours.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages