npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@go22/take-a-tour

v0.1.26

Published

Accessible, declarative product tours for React applications

Downloads

152

Readme

@go22/take-a-tour

An accessible, declarative product-tour engine for React. It can annotate an existing page or render application-owned shells with fixture data for pages a visitor cannot safely or normally access.

Release status: 0.1.10 is published under the latest npm tag.

License

This project is licensed under the GNU General Public License, version 3 only (GPL-3.0-only). See LICENSE.

Table of contents

Install

Published version

npm install @go22/[email protected]

Local package testing

Pack 0.1.10 from this package directory, then install the resulting archive in the consuming project:

# In react-tour/
npm run check
npm pack --pack-destination /tmp

# In the consuming project
npm install --force --no-save --package-lock=false /tmp/go22-take-a-tour-0.1.10.tgz

Do not install the package directory directly with npm install ../... npm links a local directory instead of installing it like a registry package, which can make an application load separate React instances and fail at runtime.

React and React DOM 18 or newer are peer dependencies. Import the package CSS once in the application entry point:

import "@go22/take-a-tour/styles.css";

Initialize a project

The take-a-tour-init executable creates or updates the integration files for a standalone example:

package.json
index.html
main.jsx
vite.config.js
steps.js

Run the initializer from the npm registry:

npm exec --package @go22/take-a-tour take-a-tour-init
npm install
npm run build

An existing valid package.json is merged with the required scripts and dependencies. Existing main.jsx, vite.config.js, and steps.js files are skipped. If index.html already exists, the matching sample page is written as index_sample.html instead. Pass --force to replace generated integration files intentionally:

npm exec --package @go22/take-a-tour take-a-tour-init --force

The generated manifest includes npm run init, npm run dev, and npm run build. The generated sample page and steps.js demonstrate uncounted and counted structural paths plus page-owned real input. The Vite pre-hook supplies a missing main.jsx module entrypoint during the build.

Included templates

The package contains two runnable templates under examples/:

examples/
|-- basic-react/   Normal Vite React application
`-- single-html/   Static page compiled to one self-contained HTML file

Each template has its own package.json and README. Copy a template outside node_modules before adapting it because a later package install can replace installed package contents.

Both examples keep the demonstrated markup ordinary:

<button type="button">Create project</button>

The tour definition locates that element structurally and adds the selector it will use for spotlighting:

{
  path: "body.main.button",
  spotlightSelector: ".tour-create-project",
}

Basic tour

Render the page without tour-specific classes or attributes:

export function Dashboard() {
  return (
    <main>
      <h1>Dashboard</h1>
      <button type="button">Create project</button>
    </main>
  );
}

Define sequential steps in application code. This example assumes the React application is mounted in a div under body:

import { defineTour } from "@go22/take-a-tour";

export const productTour = defineTour({
  steps: [
    {
      path: "body.div.main.h1",
      title: "Your dashboard",
      body: "Active projects and recent activity appear here.",
      auto: { dwellMs: 5000 },
      interactive: { waitFor: "next-click" },
    },
    {
      path: "body.div.main.button",
      title: "Create a project",
      body: "Start a new project from this button.",
      auto: { dwellMs: 5000, holdAtEnd: true },
      interactive: { waitFor: "next-click" },
    },
  ],
});

Mount one provider. It supplies the portal and tour UI; no separate tour root is needed:

import { TourEntryButton, TourProvider } from "@go22/take-a-tour";
import { productTour } from "./tour.js";

export function App() {
  return (
    <TourProvider tour={productTour}>
      <Dashboard />
      <TourEntryButton>Take a tour</TourEntryButton>
    </TourProvider>
  );
}

Tour definition

defineTour() provides generic type checking for shell names and step data and returns the public TourConfig shape.

| Option | Default | Purpose | |---|---|---| | steps | required | Ordered, non-empty array of TourStep objects | | shells | none | Map of shell names to React shell components | | remountOnStepChange | [] | Shell names whose component instance is keyed by step index | | defaultCadence | 1 | Initial timing multiplier from 0.5 through 2 | | defaultMode | "auto" | Mode selected by start() and initially selected on the start screen | | colorScheme | "default" | "default" or "random" provider accent palette | | ttloc | none | CSS selector that relocates a rendered TourEntryButton | | startOnLoad | none | "default", "auto", or "interactive" mount behavior | | labels | built-in English labels | Partial overrides for all public UI labels | | getStepAnnouncement | built-in step announcement | Generates screen-reader text from step, position, and total |

Step sequence and identity come from array order. A TourStep needs no id or key; use remountOnStepChange when a shell must remount between steps.

Provider options

| Prop | Required | Purpose | |---|---|---| | tour | yes | The result of defineTour() | | children | yes | The application or demonstrated content | | portalContainer | no | Portal destination; defaults to document.body | | className | no | Additional class on the active .rt-canvas |

Changing portalContainer changes where the tour canvas is portaled, not which document is searched for selectors. useTour() must be called below the provider.

Starting on page load

Set startOnLoad when the provider should launch the tour on its first mount:

const tour = defineTour({
  startOnLoad: "default",
  steps,
});

| Value | Page-load behavior | |---|---| | "default" | Opens the start screen so the user can choose mode and pace | | "auto" | Skips the start screen and starts Auto mode | | "interactive" | Skips the start screen and starts Interactive mode |

Omit it to wait for TourEntryButton, open(), or start(). An entry button can remain on an auto-starting page so the user can replay the tour.

Launcher placement

TourEntryButton normally renders in a fixed surface at the lower right. Set ttloc to portal that same rendered button into the first matching element:

const tour = defineTour({
  ttloc: "main",
  steps,
});

A relocated button is an unenclosed, underlined Take-a-Tour control. Missing, empty, malformed, or unmatched selectors, including the string "null", use the lower-right fallback. Void elements such as input and img cannot receive the button and also use the fallback. ttloc only relocates a rendered TourEntryButton; it does not create one, resolve paths, or affect step targeting.

Programmatic control

useTour() exposes a stable controller for custom launchers and controls:

import { useTour } from "@go22/take-a-tour";

function HelpButton() {
  const tour = useTour();
  return (
    <button onClick={() => tour.start({ mode: "interactive", stepIndex: 1 })}>
      Help
    </button>
  );
}

The state fields are active, configuring, currentStepIndex, mode, cadence, and playing. Methods are open, start, close, next, previous, goTo, setMode, setCadence, pause, and resume.

open() shows the start screen. start() begins at its requested zero-based stepIndex, or the first step, with optional mode and cadence. Controller next() closes the tour when called on the final step. Controller previous() does nothing on the first step. goTo() ignores invalid indexes and pauses timed playback. setCadence() clamps values to 0.5 through 2.

Tour UI and navigation

There is no separate top bar. The current step appears in one explainer card positioned to avoid the spotlight when possible. Its upper completion row contains the step title plus a reciprocal Auto/Interactive mode link, Restart, and explicit Exit controls. The message sits below it in a centered, rounded one-pixel bordered panel with font-weight: 500.

In Interactive mode, the card's lower action row contains Prev, Contents, and Next. Prev is disabled on the first step, Next is disabled on the final step, and neither control wraps. Exit closes the interactive tour. Contents opens the step list and selecting an item pauses playback. Prev and Next use the active accent; Contents uses #7C3AED.

In Auto mode, the lower area contains pause/resume and pace controls instead of step buttons. Pace appears beside a centered half-width slider below the pause/resume control. A vertical countdown rail shrinks beside the message. Timed advance calls controller next(), so it closes after the final step unless the final step uses holdAtEnd. Switching modes preserves the current step.

Shells

Shells are optional application components for authenticated screens, controlled fixture states, or interactions that must not reach a live backend. They receive the active step and render inside the tour canvas.

import { defineTour, type TourShellProps } from "@go22/take-a-tour";

type Shell = "dashboard";
type StepData = { state: "empty" | "populated" };

function DashboardShell({ step }: TourShellProps<Shell, StepData>) {
  return <Dashboard projects={fixtures[step.data?.state ?? "empty"]} readOnly />;
}

export const productTour = defineTour<Shell, StepData>({
  steps: [
    {
      title: "Project progress",
      body: "Completed work is summarized here.",
      shell: "dashboard",
      data: { state: "populated" },
      spotlightSelector: ".tour-project-progress",
      auto: { dwellMs: 5000, holdAtEnd: true },
      interactive: { waitFor: "next-click" },
    },
  ],
  shells: { dashboard: DashboardShell },
});

List a shell in remountOnStepChange only when its internal state must be re-created for every step. Omit it when state should persist across consecutive steps. Shell-backed controls remain inside the normal modal canvas.

Step fields

| Field | Required | Purpose | |---|---|---| | title | yes | Non-empty heading and announcement copy | | body | yes | Non-empty explainer and announcement copy | | shell | no | Name of a registered shell component | | data | no | Opaque payload passed to the shell in step | | path | no | Structural path whose resolved element is highlighted and used for interaction | | spotlightSelector | no | Alternative selector target, or an optional annotation applied to a path target | | hideExplainer | no | Hides the card for a self-contained shell | | auto.dwellMs | yes | Positive base duration before timed advance | | auto.script | no | String value applied to the resolved target 700ms after the step starts | | auto.holdAtEnd | no | Runs scripts but prevents timed advance | | auto.playScriptInInteractive | no | Runs the timed script in Interactive mode | | interactive.waitFor | yes | "next-click" or "real-input" | | interactive.interactionSelector | no | Override that expands or changes the page interaction opening | | interactive.event | no | Overrides the inferred click, input, or change event |

Selectors and element paths

Every CSS selector target is obtained with document.querySelector() and therefore uses the first match. Structural paths use their own resolver and the resolved element is measured directly; no selector is required. Make every explicit selector unique.

A path contains dot-separated standard HTML tags. Tags are case-normalized to lowercase. An optional suffix is a one-based index among elements of that tag: section2 is the second section, while h25 is the fifth h2. The first segment is selected in document order. Every later segment selects only among the current element's direct children. An omitted index means 1.

{
  path: "body.main.section2.h2",
}

Only standard tags recognized by the package are accepted; custom-element names are not supported. Empty segments, unknown tags, zero or unsafe indexes, and unresolved direct-child paths fail silently.

When both path and spotlightSelector are present, the provider can add only a simple .class, [attribute], or [attribute="value"] selector to the element. A complex selector is not synthesized, although it still works if the existing markup already matches it. Omit path to target any selector already present in the page.

Path annotations run in a provider layout effect after React commits and rerun when the tour.steps array identity changes. Targets that mount later are not automatically retried unless that dependency changes. Added classes and attributes are not removed when a step changes or the provider unmounts. validateTour() does not parse, resolve, or otherwise check path values.

Page-owned real input

A page-annotation step can require a real interaction with existing page UI:

interactive: {
  waitFor: "real-input",
}

In Interactive mode, the resolved path element determines the opening through which the page can receive pointer input. Four transparent shield panes cover the areas above, below, left, and right of that opening. The rest of the page remains blocked. Without a path, spotlightSelector is used instead.

The event is inferred from the target: text inputs and textareas use input; selects, checkboxes, radio buttons, and file inputs use change; buttons, links, and other elements use click. Set interactive.event only to override that behavior. Set interactive.interactionSelector when the interaction opening must differ from the path target, such as exposing a card containing both an input and its Submit button.

The external target, or its first focusable descendant, receives initial focus and joins the card controls in the focus loop. Because focus and interaction intentionally cross the dialog boundary, aria-modal is omitted for this state. Page-owned input behavior applies only to steps without a shell.

The inferred or configured event is captured on document; an event target inside the interaction opening satisfies the gate. It enables Next but does not automatically advance. Until the event occurs, Next remains disabled. If the target is missing, has no measurable rectangle, or the event never fires, page access does not open and the step remains gated. Use an explicit Exit or Escape to leave that state.

Actions

auto.script is a single string applied to the resolved path or spotlight target 700ms after the step starts. The delay is fixed and has no step option. The target element determines the operation:

  • Text, number, date, time, range, color, and similar inputs receive the script as their value and dispatch input and change.
  • Textareas and editable content receive the script as text.
  • A checkbox accepts "select" or "deselect".
  • A radio input accepts a value from its named group or "sequence".
  • A select accepts an option value or "sequence".
  • A button, clickable input, link, or other clickable element accepts "click".
  • File inputs cannot be populated by scripts.

Radio and select sequences begin at 700ms and move through choices every 500ms, stopping before the current paced dwell expires. The last choice remains selected. Scripts run during active Auto playback and can also run in Interactive mode when playScriptInInteractive is true.

auto: { dwellMs: 5000, script: "Apollo" }
auto: { dwellMs: 5000, script: "click" }

"click" performs a real DOM click. Navigation, form submission, application state changes, and any other resulting effects are intentional and are the responsibility of the developer authoring the step.

Validation

Use validateTour() in consuming-application tests:

import { validateTour } from "@go22/take-a-tour";
import { productTour } from "./tour.js";

expect(validateTour(productTour)).toEqual([]);

It reports an empty tour; blank title or body; unregistered shells; non-finite or non-positive dwell times; scripts whose dwell is not greater than 700ms; empty spotlight or interaction selectors; defaultCadence outside 0.5 through 2; and unsupported startOnLoad or colorScheme values.

Validation is structural. It does not test CSS selector syntax or whether a selector matches the document. It does not inspect path, require a spotlight selector, validate script compatibility with a target, or confirm runtime page and shell behavior. An empty tour returns only the empty-tour issue.

Theme

The exact stylesheet defaults are:

:root {
  --rt-accent: #b45309;
  --rt-accent-hover: #92400e;
  --rt-accent-soft: #fffbeb;
  --rt-on-accent: #ffffff;
  --rt-surface: #ffffff;
  --rt-surface-muted: #f1f5f9;
  --rt-text: #334155;
  --rt-text-muted: #64748b;
  --rt-border: #cbd5e1;
  --rt-overlay: rgba(15, 23, 42, 0.52);
  --rt-spotlight: rgba(251, 191, 36, 0.86);
  --rt-progress: var(--rt-accent);
  --rt-radius: 0.65rem;
  --rt-font: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --rt-z-index: 3000;
}

Pass className to TourProvider to target one active canvas. All package classes use the rt- prefix.

At provider initialization, colorScheme: "default" chooses #B45309 and "random" chooses one of 12 predefined accents. The provider derives hover, soft, and contrasting foreground colors, then writes --rt-accent, --rt-accent-hover, --rt-accent-soft, --rt-on-accent, and --rt-progress as inline styles on the canvas. Those inline palette values override ordinary class or root declarations. Override them with inline-style priority such as !important, or theme the remaining variables normally. The palette is chosen once for each provider mount, not on every tour start.

Accessibility

The package provides Escape handling, focus restoration, dynamic focus discovery, step announcements, a button-list Contents panel, and keyboard focus containment. The start screen exposes mode and pace choices. Interactive page-owned input intentionally expands the focus loop and omits aria-modal as described above.

Auto mode provides pause/resume, a pace slider, and a shrinking countdown rail so timed transitions are predictable. The rail pauses with playback and is omitted in Interactive mode and on held steps. Do not remove pause from a customized Auto experience. Keep unavailable shell controls genuinely disabled so they do not enter the dialog's Tab cycle.

Quick Start: single HTML page

examples/single-html is the canonical static-page workflow. It leaves the demonstrated page as ordinary HTML and mounts React only for the provider and entry button. Vite and vite-plugin-singlefile compile JavaScript and CSS into one dist/index.html. The tour must run in the same document as path targets; an iframe cannot inspect elements in its parent document.

Use these root-level files:

index.html
main.jsx
steps.js
vite.config.js
package.json

The source index.html contains the static page only. It needs neither a React mount placeholder nor a module script because the Vite pre-hook injects the entry point:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Single-file product tour</title>
  </head>
  <body>
    <main>
      <h1>Example dashboard</h1>
      <section>
        <h2>Recent activity</h2>
        <p>Two projects were updated today.</p>
      </section>
      <section>
        <h2>Project summary</h2>
        <button type="button">Create project</button>
      </section>
      <div><h2>Saved views</h2></div>
      <div id="project-submission">
        <label for="project-name">Project name</label>
        <input id="project-name" type="text" />
        <button id="project-submit" type="button">Submit project</button>
        <p id="project-result" aria-live="polite" hidden></p>
      </div>
    </main>
    <script>
      const projectName = document.querySelector("#project-name");
      const projectResult = document.querySelector("#project-result");
      function submitProject() {
        const name = projectName.value.trim();
        if (!name) return projectName.focus();
        projectResult.textContent = `Project submitted: ${name}`;
        projectResult.hidden = false;
        projectName.value = "";
      }
      document.querySelector("#project-submit").addEventListener("click", submitProject);
      projectName.addEventListener("keydown", (event) => {
        if (event.key === "Enter") submitProject();
      });
    </script>
  </body>
</html>

Define paths against that static structure in steps.js. An omitted count selects the first matching tag, while section2 and div2 select the second direct child of each tag type:

export const startOnLoad = "default";
export const colorScheme = "random";
export const ttloc = null;

export const steps = [
  {
    path: "body.main.section",
    title: "Recent activity",
    body: "A path without a count selects the first matching section.",
    auto: { dwellMs: 5000 },
    interactive: { waitFor: "next-click" },
  },
  {
    path: "body.main.section2.button",
    title: "Create a project",
    body: "The second section contains the control for starting a project.",
    auto: { dwellMs: 5000 },
    interactive: { waitFor: "next-click" },
  },
  {
    path: "body.main.div2.input",
    title: "Name your project",
    body: "Enter a project name, then submit it below.",
    auto: { dwellMs: 5000, script: "Apollo" },
    interactive: {
      waitFor: "real-input",
      interactionSelector: "#project-submission",
    },
  },
  {
    path: "body.main.div2.button",
    title: "Submit your project",
    body: "Submit the entered project name.",
    auto: { dwellMs: 5000, script: "click" },
    interactive: { waitFor: "real-input" },
  },
];

Root main.jsx creates its mount dynamically:

import React from "react";
import { createRoot } from "react-dom/client";
import { TourEntryButton, TourProvider, defineTour } from "@go22/take-a-tour";
import "@go22/take-a-tour/styles.css";
import * as settings from "./steps";

const tour = defineTour(settings);
const mount = document.body.appendChild(document.createElement("div"));

createRoot(mount).render(
  <React.StrictMode>
    <TourProvider tour={tour}>
      <TourEntryButton>Take a tour</TourEntryButton>
    </TourProvider>
  </React.StrictMode>,
);

The ensure-main-entrypoint pre-hook avoids duplicate injection, inserts the root entry before </body>, and appends it when an input document has no closing body tag:

import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
import { viteSingleFile } from "vite-plugin-singlefile";

const ensureMainEntrypoint = {
  name: "ensure-main-entrypoint",
  transformIndexHtml: {
    order: "pre",
    handler(html) {
      if (/<script\b[^>]*\bsrc\s*=\s*(["'])\.\/main\.jsx\1[^>]*>/i.test(html)) return html;
      const entrypoint = '  <script type="module" src="./main.jsx"></script>\n';
      return /<\/body>/i.test(html)
        ? html.replace(/<\/body>/i, `${entrypoint}</body>`)
        : `${html}\n${entrypoint}`;
    },
  },
};

export default defineConfig({
  plugins: [ensureMainEntrypoint, react(), viteSingleFile()],
  build: {
    cssCodeSplit: false,
    assetsInlineLimit: Number.MAX_SAFE_INTEGER,
  },
});

Install the package using the command in Install, then run:

npm run build

The result is dist/index.html, which can be opened with a file:// URL or served as one static file. Links, remote images, fonts, network requests, and CSS URLs remain external unless imported and inlined by the build. A strict Content Security Policy must permit the generated inline script and style, usually with hashes or nonces.

Quick Start: normal Vite application

For a normal application, mount the page and provider together in the existing React entry point. index.html has a root and a normal Vite module script:

<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
import React from "react";
import { createRoot } from "react-dom/client";
import { TourEntryButton, TourProvider, defineTour } from "@go22/take-a-tour";
import "@go22/take-a-tour/styles.css";
import App from "./App.jsx";
import { steps } from "./steps.js";

const tour = defineTour({ startOnLoad: "default", steps });

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <TourProvider tour={tour}>
      <App />
      <TourEntryButton>Take a tour</TourEntryButton>
    </TourProvider>
  </React.StrictMode>,
);

Because the application is mounted under #root, paths to application markup begin with body.div, for example body.div.main.h1. A normal Vite build does not use vite-plugin-singlefile; deploy all of dist/, including generated JavaScript and CSS assets.

Development and testing

The current development package version is 0.1.10. From the package directory:

npm run typecheck
npm test
npm run build
npm run check
npm run build:example

npm run check runs type checking, the current 32-test Vitest suite, and the package build. npm run build:example regenerates ../example.html; review that generated output when the example source changes. Use --package-lock=false when installing a temporary packed archive into an example so local verification does not rewrite its lockfile.

Scope and routing

This release supports one mounted document plus application-owned shells. It does not navigate routes or HTML pages. Coordinate routing in application code before starting the relevant tour, or render route states as shells. For a true multiple-HTML Vite build, configure every HTML file as a Vite input and mount or import the integration on each page; each step target must exist in the current document or in the active shell.