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

a11y-tour-guide

v1.0.0

Published

A framework-agnostic, accessible guided tour for the web.

Readme

a11y-tour-guide

a11y-tour-guide adds accessible, keyboard-friendly product tours to a web page. It uses plain browser APIs, so it works with vanilla JavaScript and with frameworks such as React, Vue, and Svelte.

The library manages the tour dialog, focus, keyboard controls, target highlighting, translations, right-to-left layouts, and missing targets.

Explore the project

The repository demo is organized to make the most important behavior easy to verify before the implementation detail gets in the way:

  • Guided Proof demo starts only on a user action and walks through focus, keyboard, and recovery checks.
  • API reference holds the complete step, controller, event, label, and theme contract.
  • Interaction laboratory isolates advanced states such as missing targets, guards, async setup, modeless behavior, and skins.
  • Basic example is a small release-handoff orientation that imports the built package output.

Installation

Install the project dependencies when working with this repository:

npm install
npm run dev

When using the published package in another project:

npm install a11y-tour-guide
pnpm add a11y-tour-guide
yarn add a11y-tour-guide

Basic use

Import the library and its styles, create a tour, then start it from a button.

import { createTour } from "a11y-tour-guide";
import "a11y-tour-guide/styles.css";

const tour = createTour({
  steps: [
    {
      id: "welcome",
      target: "#welcome",
      title: "Welcome",
      content: "This is the first stop in the tour."
    },
    {
      id: "settings",
      target: "#settings",
      title: "Settings",
      content: "Change your preferences here."
    }
  ]
});

document.querySelector("#start-tour").addEventListener("click", () => {
  void tour.start();
});

Each step needs a unique id, a target, a title, and some content.

HTML structure

The page markup stays semantic and application-owned. A tour needs stable target elements and a user-operated control that calls start():

<button id="start-tour" type="button">Start tour</button>

<main>
  <h1 id="welcome">Welcome</h1>
  <section id="settings" aria-labelledby="settings-title">
    <h2 id="settings-title">Settings</h2>
    <p>Change your preferences here.</p>
  </section>
</main>

The library generates the dialog, overlay, highlight, live status region, and optional checklist checkbox when the tour starts. Do not hard-code those generated elements in your page markup.

CSS skins

The default stylesheet is still the easiest option. It includes the structural CSS and the classic skin:

import "a11y-tour-guide/styles.css";

For a custom skin setup, import the base CSS once, import one or more theme files, then set the theme option. The generated tour wrapper receives the namespaced data-a11y-tour-theme attribute:

import "a11y-tour-guide/base.css";
import "a11y-tour-guide/themes/console.css";
import "a11y-tour-guide/themes/sticky-note.css";

const tour = createTour({
  theme: "console",
  steps
});

Ready-to-use skins:

| Skin | Import | Theme value | | --- | --- | --- | | Classic | a11y-tour-guide/themes/classic.css | classic | | Basic | a11y-tour-guide/themes/basic.css | basic | | Brutalist | a11y-tour-guide/themes/brutalist.css | brutalist | | Blueprint | a11y-tour-guide/themes/blueprint.css | blueprint | | Sticky note | a11y-tour-guide/themes/sticky-note.css | sticky-note | | Console | a11y-tour-guide/themes/console.css | console | | Spotlight | a11y-tour-guide/themes/spotlight.css | spotlight | | Checklist | a11y-tour-guide/themes/checklist.css | checklist | | Glass | a11y-tour-guide/themes/glass.css | glass | | Compact Pro | a11y-tour-guide/themes/compact-pro.css | compact-pro | | High contrast | a11y-tour-guide/themes/high-contrast.css | high-contrast |

Use basic when you want the least opinionated starting point for a CMS, white-label product, or site builder integration. It keeps the component variables and accessible focus/touch defaults, but avoids decorative framing, heavy shadows, custom layout treatments, and pseudo-element styling.

Legacy values and imports still work: dark maps to console, soft-light maps to sticky-note, and compact maps to compact-pro. Unknown theme values fall back to classic.

The documented public CSS variables are:

| Variable | Purpose | | --- | --- | | --a11y-tour-color-surface | Dialog surface color | | --a11y-tour-color-text | Main text color | | --a11y-tour-color-muted | Secondary text color | | --a11y-tour-color-accent | Primary action and theme accent | | --a11y-tour-color-border | Default border color | | --a11y-tour-color-overlay | Modal overlay color | | --a11y-tour-radius | Base corner radius | | --a11y-tour-shadow | Dialog elevation | | --a11y-tour-font-family | Dialog font stack | | --a11y-tour-z-index | Stacking fallback when the root z-index is customized in CSS |

Override tokens after importing the theme CSS:

.product-onboarding {
  --a11y-tour-color-accent: #005fcc;
  --a11y-tour-radius: 0.75rem;
  --a11y-tour-shadow: 0 1rem 2rem rgb(0 0 0 / 0.22);
}

Each theme also uses private --_tour-* variables internally. Treat those as implementation details; prefer the public variables above for app overrides.

Theme QA notes:

  • Keyboard: the interaction laboratory's skin radios are native controls, and tour buttons stay reachable.
  • Focus: buttons, checklist inputs, dialogs, and laboratory skin cards keep visible focus.
  • Screen readers: title, content, progress text, and live updates remain in the dialog contract.
  • Motion: highlight, overlay, and decorative transitions stop for reduced motion.
  • Forced colors: dialogs, buttons, selected cards, and highlights use visible borders.
  • RTL and mobile: theme spacing uses logical properties and remains usable at 320px.

Generated selectors and attributes:

| Selector or attribute | Purpose | | --- | --- | | .a11y-tour-root | Fixed-position generated tour wrapper. | | .a11y-tour-overlay | Modal backdrop, present only for modal tours. | | .a11y-tour-dialog | Labelled dialog surface that receives focus. | | .a11y-tour-highlight | Visual target frame with aria-hidden="true". | | .a11y-tour-live | Polite status region for step and close announcements. | | .a11y-tour-button | Generated close, skip, previous, and next controls. | | .a11y-tour-checklist-input | Optional native checkbox for checklist mode. | | data-a11y-tour-theme | Theme selector applied to the generated root. |

Step options

| Option | What it does | | --- | --- | | id | A unique name for the step. You can also use it with goTo(). | | target | The element to highlight. Use a CSS selector, an element, or a function that returns an element. | | title | The heading shown in the tour dialog. | | content | The main text shown in the tour dialog. | | placement | Preferred dialog position: "top", "right", "bottom", "left", or "auto". | | locales | Only include this step for specific languages, such as ["en", "fr"]. | | beforeShow | Run a function before the step appears. It may be asynchronous. | | canAdvance | Return false to stop the user moving to the next step. It may be asynchronous. | | canAdvanceMessage | Announces why the user cannot move on when canAdvance returns false. |

Different kinds of targets

Use a selector for an element that already exists:

{
  id: "profile",
  target: "#profile-button",
  title: "Your profile",
  content: "Open your account settings here."
}

Use an element directly:

{
  id: "search",
  target: document.querySelector("#search"),
  title: "Search",
  content: "Search the site from here."
}

Use a function for content that may appear later:

{
  id: "results",
  target: () => document.querySelector(".search-results"),
  title: "Results",
  content: "Your latest results appear here."
}

If a target is missing or hidden while a step is being shown, the tour makes one polite status announcement that names each consecutively unavailable target, then identifies the displayed fallback step and its progress. This avoids losing the skip reason when the fallback step is rendered.

Refresh after a framework render

Use refresh() after your framework has committed a replacement for the currently highlighted target. Prefer a selector or a function target for this case: an element target is a reference to that exact DOM node and cannot find its replacement.

// Run after the component has rendered its updated DOM.
const refreshed = tour.refresh();

if (!refreshed) {
  // The application decides whether to retry, show another view, or close the tour.
}

For example, call it from a React useLayoutEffect, Vue nextTick, or the equivalent post-render hook only when the current target may have changed. refresh() does not run beforeShow, navigate to another step, scroll the page, move focus, or announce anything when it succeeds. It resolves one target, reads layout to reposition the dialog and highlight, then reconnects the target observer.

If the target is unavailable, refresh() returns false, hides the stale highlight, disconnects its observer, and keeps the current step open. It announces that unavailable state once until a later refresh finds a target; the application remains in control of recovery.

Multiple dynamic elements

Create one step for each dynamic element before creating the tour:

const dynamicSteps = Array.from(
  document.querySelectorAll(".dynamic-card")
).map((card, index) => ({
  id: `dynamic-${index + 1}`,
  target: card,
  title: card.querySelector("h2")?.textContent ?? `Item ${index + 1}`,
  content: card.querySelector("p")?.textContent ?? "Dynamic content"
}));

const tour = createTour({
  steps: [
    {
      id: "welcome",
      target: "#welcome",
      title: "Welcome",
      content: "Start the tour here."
    },
    ...dynamicSteps
  ]
});

Create or rebuild the tour after the dynamic elements have been added. A single selector such as ".dynamic-card" selects only the first matching element.

Tour options

Only steps is required. All other options have sensible defaults.

| Option | Default | What it does | | --- | --- | --- | | steps | Required | The ordered list of tour steps. | | theme | Page theme or "classic" | Sets data-a11y-tour-theme on the generated tour root. Legacy values are normalized. | | locale | Page language or "en" | Language used for labels and step filtering. | | fallbackLocale | "en" | Language to use when a translation is missing. | | translations | {} | Translated button labels and announcements. | | labels | English labels | Replaces individual labels without adding a full translation. | | direction | "auto" | Text direction: "ltr", "rtl", or automatic. | | placement | "auto" | Default dialog position for all steps. | | modal | true | Prevents interaction with the page behind the tour. | | scrollToTarget | true | Scrolls each highlighted target into view. | | smoothScroll | true | Uses smooth scrolling when motion is allowed. | | reducedMotion | false | Disables tour motion when set to true. | | keyboardNavigation | true | Allows the arrow keys to move between steps. | | closeOnEscape | true | Allows Escape to close the tour. | | showProgress | true | Shows text such as “Step 2 of 4”. | | checklist | false | Shows a native checkbox for marking each step complete. | | className | "" | Adds a custom class to the tour root for styling. | | zIndex | 10000 | Controls whether the tour appears above other page content. | | on | {} | Event handlers for tour activity. |

Example with a few common options:

const tour = createTour({
  theme: "spotlight",
  placement: "bottom",
  modal: true,
  closeOnEscape: true,
  showProgress: true,
  className: "my-product-tour",
  zIndex: 20000,
  steps
});

Control the tour

createTour() returns a controller with these methods:

| Method | What it does | | --- | --- | | start() | Starts at the first step. | | start(2) | Starts at a step number. Step numbers start at 0. | | start("settings") | Starts at a step with that id. | | next() | Moves to the next step. | | previous() | Moves to the previous step. | | goTo(2) | Moves to a step number. | | goTo("settings") | Moves to a step by id. | | refresh() | Re-resolves and repositions the current target. Returns false when the tour is inactive or destroyed, or its current target is unavailable. | | skip() | Ends the tour as skipped. | | complete() | Ends the tour as completed. | | destroy() | Closes the tour and permanently disposes of this controller. | | isActive() | Returns true while the tour is open. | | getCurrentStep() | Returns the current step, or null. | | isStepCompleted("settings") | Returns whether a step has been marked complete. | | getCompletedSteps() | Returns completed step IDs in tour order. | | setStepCompleted("settings", true) | Marks a step complete or incomplete. | | clearCompletedSteps() | Clears all completed step IDs. | | on(name, handler) | Adds a typed lifecycle observer and returns an unsubscribe function. | | off(name, handler) | Removes a lifecycle observer explicitly. |

The navigation methods are asynchronous:

await tour.start();
await tour.goTo("settings");
await tour.next();

Do not reuse a controller after calling destroy(). Create a new tour instead.

API

The main runtime API is the createTour(config) function. It returns a TourController, and the A11yTourGuide class is also exported for advanced direct construction. TypeScript users can import the TourConfig, TourStep, TourController, TourLabels, and related event types from the package root.

Events

Use the on configuration option for observers known when the controller is created. Shared constants avoid spelling event names as literals:

import { TOUR_EVENTS, createTour } from "a11y-tour-guide";

const tour = createTour({
  steps,
  on: {
    [TOUR_EVENTS.start]: ({ step }) => {
      console.log("Tour started at", step.id);
    },
    [TOUR_EVENTS.stepChange]: ({ step, index, reason }) => {
      console.log("Showing step", index, step.id, reason);
    },
    [TOUR_EVENTS.completionChange]: ({ step, completed, source }) => {
      console.log(step.id, completed ? "complete" : "incomplete");
      console.log("Changed by", source);
    },
    [TOUR_EVENTS.complete]: () => {
      console.log("Tour completed");
    },
    [TOUR_EVENTS.skip]: () => {
      console.log("Tour skipped");
    },
    [TOUR_EVENTS.close]: () => {
      console.log("Tour closed");
    },
    [TOUR_EVENTS.error]: ({ error, operation }) => {
      console.error(operation, error);
    },
    [TOUR_EVENTS.destroy]: () => {
      console.log("Tour destroyed");
    }
  }
});

Add observers later with the typed controller methods:

const unsubscribe = tour.on(
  TOUR_EVENTS.completedStepsChange,
  ({ completedStepIds, previousCompletedStepIds }) => {
    renderProgress(completedStepIds, previousCompletedStepIds);
  }
);

// Remove the observer when its owning integration is disposed.
unsubscribe();
// tour.off(TOUR_EVENTS.completedStepsChange, handler) is also available.

These are synchronous plain callbacks, not DOM CustomEvents. There is no event target and no bubbling, composed, or cancelable behavior.

| Event | When it runs | Detail | | --- | --- | --- | | start | Once after an inactive tour commits its first visible step | step, index | | stepchange | After any visible step commits | step, index, previousIndex, reason | | completionchange | Once per effectively changed step | step state, completed-ID snapshot, source, reason | | completedstepschange | Once after an effective single or batch completion mutation | previous/current snapshots, changed IDs, source, reason | | complete | After completed-tour teardown | reason: "completed" | | skip | After skipped-tour teardown | reason: "skipped" | | close | After close-button or Escape teardown | reason: "closed" | | error | After a failed navigation operation closes and cleans up the tour | error, operation, optional step | | destroy | Once after permanent controller destruction | reason: "destroyed" |

Ordering and async guarantees:

  • Initial activation emits start and then stepchange. Calling start() while already active may navigate, but emits only stepchange.
  • Terminal and error callbacks observe isActive() === false and a removed generated root.
  • clearCompletedSteps() retains ordered item-level completionchange callbacks, then emits one aggregate completedstepschange.
  • Closing or destroying during beforeShow or canAdvance invalidates that operation. Stale work emits no final lifecycle callbacks.
  • After destroy(), no lifecycle callback other than that single destroy observation can run.
  • All observers for one event are attempted. Observer failures propagate to the caller after state commit, but do not become plugin error callbacks or prevent cleanup.

TypeScript users can import the named detail interfaces, TourEventMap, TourEventName, and TourEventHandler from the package root.

Synchronize steps with the URL

Import the optional URL adapter from its separate package entry when a tour needs shareable or browser-history-aware steps:

import { createTour } from "a11y-tour-guide";
import { createUrlStepSync } from "a11y-tour-guide/url-sync";

const tour = createTour({ steps });
const urlSync = createUrlStepSync(tour, {
  mode: "query",
  key: "tour-step",
  history: "replace"
});

document.querySelector("#start-tour").addEventListener("click", () => {
  void urlSync.start();
});

Creating the adapter never starts the tour. urlSync.start() must be called from an explicit application or user action. It starts at a valid URL step, falls back to the normal first step when the URL value is unknown, and then keeps committed stepchange events synchronized. Browser Back and Forward changes call the controller's normal goTo() path only while the tour is active, preserving its existing focus and announcement behavior.

The adapter options are:

| Option | Default | Purpose | | --- | --- | --- | | mode | "query" | Stores the step in the URL query or parameter-style hash. | | key | "tour-step" | Names the query or hash parameter. | | history | "replace" | Uses replaceState; set "push" only when every step should create a Back/Forward entry. |

The returned adapter provides:

| Method | Purpose | | --- | --- | | readStep() | Reads the configured URL value without starting or navigating. | | start(startAt?) | Explicitly starts at the URL step, then falls back to startAt or the first step. | | sync() | Requests navigation from the current URL while the tour is active. | | destroy() | Removes tour observers and browser-history listeners. |

Query mode preserves unrelated query parameters and the existing fragment. Hash mode uses URLSearchParams syntax such as #section=account&tour-step=settings; it should not be used on pages whose fragment is owned by traditional anchors or another router. Missing URL values are ignored, terminal tour states preserve the last step, and destroying the tour also cleans up the adapter.

Step IDs written to a URL are public navigation data. Query values may appear in server logs, referrer information, copied links, and analytics; hashes may still be collected by client-side analytics. Do not use titles, selectors, form values, account information, or other sensitive data as step IDs. The adapter uses no storage, network requests, analytics, or runtime dependencies.

See examples/url-sync for a complete packaged example.

Let users tick off steps

Set checklist: true to add a native checkbox to each step. Checked steps are tracked by step id; they do not block Next or Finish.

const tour = createTour({
  checklist: true,
  steps
});

Use optional persistence when a checklist should survive closing and reopening the page:

const tour = createTour({
  checklist: {
    persist: true,
    storageKey: "product-onboarding-checklist",
    resumeIncomplete: true
  },
  steps
});

Persisted step IDs are stored in localStorage. If storage is unavailable, the checklist still works for the current page session. When resumeIncomplete is enabled, start() opens the first incomplete step, normal Next and Previous navigation skips checked steps, and checking the current step advances to the next incomplete step. Progress also reflects the remaining incomplete path, such as Step 1 of 2 after two steps in a four-step tour are complete. Explicit calls such as start("settings") still choose that exact step.

Require an action before continuing

Use canAdvance when the user must finish something before selecting Next:

{
  id: "accept",
  target: "#accept-terms",
  title: "Accept the terms",
  content: "Select the checkbox before continuing.",
  canAdvanceMessage: "Select the checkbox before continuing.",
  canAdvance: () => {
    return document.querySelector("#accept-terms").checked;
  }
}

Use beforeShow when a panel or menu needs to open before its step:

{
  id: "menu-item",
  target: "#menu-item",
  title: "Menu item",
  content: "This item is inside the menu.",
  beforeShow: async () => {
    await openMenu();
  }
}

Change labels

Use labels for small text changes:

const tour = createTour({
  labels: {
    next: "Continue",
    previous: "Back",
    finish: "Done"
  },
  steps
});

The available labels are:

  • next
  • previous
  • skip
  • close
  • finish
  • progress
  • stepAnnouncement
  • completedAnnouncement
  • skippedAnnouncement
  • closedAnnouncement
  • missingTargetAnnouncement
  • blockedAnnouncement
  • markStepComplete

Templates can use {current}, {total}, and {title} where appropriate.

Languages and right-to-left text

Use locale and translations to translate controls and announcements:

const tour = createTour({
  locale: "fr",
  fallbackLocale: "en",
  translations: {
    fr: {
      next: "Suivant",
      previous: "Précédent",
      skip: "Ignorer la visite",
      close: "Fermer la visite",
      finish: "Terminer",
      progress: "Étape {current} sur {total}"
    }
  },
  steps
});

Set direction: "auto" to choose left-to-right or right-to-left from the locale. You may also set it directly to "ltr" or "rtl".

Use locales on a step when that step should only appear in some languages:

{
  id: "french-help",
  target: "#french-help",
  title: "Aide",
  content: "Aide supplémentaire en français.",
  locales: ["fr", "fr-CA"]
}

Accessibility behavior

a11y-tour-guide provides the following behavior:

  • Focus moves into the labelled dialog for each step.
  • Focus returns to the element that started the tour when it closes.
  • Tab and Shift+Tab stay inside a modal tour.
  • Escape closes the tour when closeOnEscape is enabled.
  • Arrow keys move between steps when keyboard navigation is enabled.
  • The dialog exposes its title, content, progress, and status announcements.
  • Missing or hidden targets are announced together with the fallback step; verify that the skip reason and the displayed step are both conveyed in a screen reader.
  • Blocked steps announce why Next did not move forward.
  • Checklist mode uses a native labelled checkbox for each step.
  • Skip and close controls remain available throughout the tour.
  • Modal tours make the rest of the page inert and stop page scrolling.
  • When modal tours overlap, background isolation remains active until the last modal closes; only the topmost modal handles keyboard input, focus stays in a surviving dialog, and focus finally returns to the original launcher when it is still available.
  • The target highlight does not block pointer input.
  • The dialog stays inside the viewport and repositions when the page changes.
  • Motion follows prefers-reduced-motion.

Present one guided tour at a time in normal product flows. Concurrent tours are handled safely for cleanup and integration edge cases, but competing dialogs can still create confusing instructions for users.

The library cannot make the tour content accessible by itself. Use clear titles, short instructions, stable selectors, and a logical step order. Do not rely only on color or visual position. Test with a keyboard, screen reader, zoom, high contrast mode, translated content, and representative browsers.

Browser support

The package targets modern browsers and ES2022. It uses browser features such as inert, ResizeObserver, and matchMedia. Add polyfills if you need to support older browsers.

Project commands

| Command | What it does | | --- | --- | | npm run dev | Starts the demo development server. | | npm run build | Builds the library and refreshes the GitHub Pages site in docs. | | npm run build:dist | Builds the JavaScript library, CSS, source maps, and TypeScript declarations in dist. | | npm run build:library | Alias for npm run build:dist. | | npm run pages:generate | Compiles the demo to the branch-publishable docs folder. | | npm run pages:build | Builds package artifacts and then regenerates docs. | | npm run build:demo | Alias for npm run pages:generate. | | npm run preview | Serves the generated docs site locally. | | npm run lint | Checks the source with ESLint. | | npm test | Runs the unit tests. | | npm run test:e2e | Runs the Chromium accessibility and interaction tests. | | npm run typecheck | Checks TypeScript without emitting files. | | npm run pack:check | Runs npm pack --dry-run to inspect package contents. | | npm run check | Runs lint, unit tests, and the production build. |

GitHub Pages

The static demo is generated into docs/ so GitHub Pages can publish it directly from the repository branch. Its asset URLs are relative, so the site works at the repository subpath used by project Pages sites.

Before committing a demo change, regenerate the publishable site:

npm run pages:build

Then configure the repository once in Settings → Pages:

  1. Under Build and deployment, select Deploy from a branch.
  2. Select the branch to publish (usually main).
  3. Select the /docs folder and save.

Commit docs/index.html, docs/assets/, and docs/.nojekyll. The package output in dist/ remains ignored and is not the GitHub Pages source.

Examples

  • Guided Proof demo is the homepage: an explicit-start tour with four browser checks for intentional start, focus, keyboard operation, and recovery.
  • API reference contains the full public contract without the interactive scenarios around it.
  • Interaction laboratory provides isolated advanced behavior and skin checks.
  • Basic example imports from the built dist output for a release-handoff orientation.
  • URL synchronization example demonstrates opt-in, explicit-start URL step synchronization.

Docs metadata

The package exports a docs metadata object for documentation aggregators:

import { docs } from "a11y-tour-guide/docs";

It includes installation commands, keyboard behavior, generated selectors, public API and event summaries, and links to the repository examples.

License

MIT