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

@brainfish-ai/wayfinder-core

v0.1.10

Published

A framework-agnostic product tour library with spotlight overlay and analytics hooks

Readme

@wayfinder/core

A framework-agnostic, TypeScript-first product tour library. Spotlight your UI elements, guide users step by step, and hook into every interaction for analytics — with no framework dependencies.

Features

  • Spotlight overlay — smooth CSS-transition cutout that glides between elements
  • Smart tooltip positioning — powered by Floating UI; respects screen edges, scrolling, and all 12 placements
  • Analytics event hooks — typed callbacks at every user interaction (onStart, onNext, onPrev, onSkip, onComplete, onError)
  • Keyboard navigation / Enter advance, goes back, Escape skips
  • Themeable — override any CSS class via the theme config; no Tailwind required in your app
  • Single active tour guard — only one tour can run at a time; Wayfinder.isRunning() lets you gate programmatically
  • Obfuscated class supportStepTarget accepts CSS selectors, direct refs, or a lazy resolver function; tourId() helper + data-tour convention; waitForTarget MutationObserver retry for SPA async DOM
  • Zero framework dependency — plain DOM manipulation; works with React, Vue, Angular, Svelte, or vanilla JS
  • Tiny — ~14 kB ESM, ~12 kB CJS (gzipped: ~4 kB)

Installation

npm install @wayfinder/core @floating-ui/dom
# or
yarn add @wayfinder/core @floating-ui/dom

@floating-ui/dom is a peer dependency and must be installed alongside the library.

Quick start

import { Wayfinder } from '@wayfinder/core';

const tour = new Wayfinder({
  steps: [
    {
      id: 'welcome',
      target: '#dashboard-header',
      title: 'Welcome to your Dashboard',
      description: 'Here you can see a high-level overview of your stats.',
      placement: 'bottom',
    },
    {
      id: 'create-button',
      target: '#btn-create-new',
      title: 'Create a new project',
      description: 'Click here to get started.',
      placement: 'left',
    },
  ],
});

document.getElementById('start-tour')!.addEventListener('click', () => {
  tour.start();
});

Wayfinder injects its own styles at runtime — no CSS import required.

Configuration

new Wayfinder(config: WayfinderConfig)

WayfinderConfig

| Property | Type | Required | Description | |---|---|---|---| | steps | Step[] | One of steps or flows | Ordered steps for a single-flow tour. Acts as a fallback when flows is also provided and no flow matches the current profile. | | flows | Flow[] | One of steps or flows | Multiple named flows, each with a profile matcher and its own step set. Evaluated in order at start() time; first match wins. | | profile | UserProfile | No | Arbitrary key-value object passed to each Flow.match() function and to per-step condition / next callbacks. Defaults to {}. | | theme | Theme | No | CSS class overrides and visual options. | | events | EventHandlers | No | Analytics / lifecycle callbacks. | | waitForTargetTimeoutMs | number | No | Max time (ms) to wait for a target element to appear in the DOM (uses a MutationObserver). Fires onError on timeout. Default: 3000. | | tourAttribute | string | No | The HTML attribute used by tourId() and the framework helpers. Change if data-tour conflicts with another library. Default: 'data-tour'. | | launcher | LauncherConfig | No | Optional self-serve launcher beacon. A pulsing ring is attached to the target element; clicking it starts the tour automatically. |


Flow

A named tour variant tied to a user profile. Flows let you show a different set of steps depending on user state — role, plan, onboarding status, etc.

| Property | Type | Required | Description | |---|---|---|---| | id | string | Yes | Unique identifier — surfaced in all analytics payloads as flowId. | | match | (profile: UserProfile) => boolean | Yes | Called with the current profile at start() time. Return true to select this flow. Flows are tested in array order; first truthy match wins. | | steps | Step[] | Yes | Steps shown when this flow is matched. |

Example — role-based flows:

const tour = new Wayfinder({
  profile: { role: 'admin', plan: 'enterprise' },
  flows: [
    {
      id: 'admin-onboarding',
      match: (profile) => profile.role === 'admin',
      steps: [
        { id: 'users', target: '#users-nav', title: 'User management', description: 'Manage your team here.' },
        { id: 'billing', target: '#billing-nav', title: 'Billing', description: 'View and update your subscription.' },
      ],
    },
    {
      id: 'member-onboarding',
      match: () => true, // fallback flow
      steps: [
        { id: 'dashboard', target: '#dashboard', title: 'Your Dashboard', description: 'Everything starts here.' },
      ],
    },
  ],
});

Step

| Property | Type | Required | Description | |---|---|---|---| | id | string | Yes | Unique identifier (used in analytics payloads). | | target | StepTarget | Yes | See StepTarget below. | | title | string | Yes | Tooltip heading. | | description | string | Yes | Tooltip body text. | | placement | Placement | No | Floating UI placement (default: 'bottom'). Any of: top, bottom, left, right, and their -start / -end variants. | | spotlightPadding | number | No | Extra padding (px) around the spotlight cutout for this step. | | condition | (profile: UserProfile) => boolean | No | Guard evaluated just before the step is displayed. Return false to skip the step and auto-advance in the current direction. | | next | string \| ((profile: UserProfile) => string \| null \| undefined) | No | ID of the step to show after this one, overriding default linear progression. Return null / undefined from the function to complete the tour early. | | clickOnNext | boolean | No | When true, pressing Next programmatically clicks the step's target element before advancing. Useful for demo flows. Default: false. | | url | string \| (() => string) | No | URL or path this step belongs to. Wayfinder navigates to it if the current URL doesn't match. Supports :param and * wildcards for pattern matching without triggering navigation. |

condition example — admin-only step:

{
  id: 'invite-team',
  target: '[data-tour="invite-btn"]',
  title: 'Invite your team',
  description: 'Add teammates with a single click.',
  condition: (profile) => profile.role === 'admin',
}

next example — dynamic branching:

{
  id: 'check-integration',
  target: '[data-tour="integrations"]',
  title: 'Integrations',
  description: 'Connect your tools.',
  next: (profile) => profile.hasIntegration ? 'step-done' : 'step-connect',
}

clickOnNext example — demo walkthrough:

{
  id: 'open-menu',
  target: '[data-tour="menu-btn"]',
  title: 'Open the menu',
  description: 'Click here to open the navigation menu.',
  clickOnNext: true, // clicks the element then advances
}

url example — cross-page tour:

[
  { id: 'settings', target: '[data-tour="settings-tab"]', url: '/settings', title: 'Settings', description: '...' },
  { id: 'profile',  target: '[data-tour="profile-form"]', url: '/settings/profile', title: 'Profile', description: '...' },
]

StepTarget

type StepTarget =
  | string                      // CSS selector: '#my-btn', '[data-tour="create"]'
  | HTMLElement                 // Direct element reference
  | (() => HTMLElement | null); // Lazy resolver — called at step-show time

The resolver function form is the escape hatch for obfuscated class names. It is called freshly at every step transition, so it works with CSS Modules, styled-components, React refs, and any other dynamic element source:

// CSS Modules — resolve by data-testid instead of the hashed class
{ target: () => document.querySelector('[data-testid="create-button"]') }

// React ref captured in outer scope
{ target: () => myRef.current }

// styled-components — target by a stable attribute you control
{ target: () => document.querySelector('[data-tour="create-btn"]') }

Theme

All class properties replace the default class string when provided. Use this to supply your own Tailwind or custom CSS classes.

| Property | Type | Description | |---|---|---| | tooltip | string | Tooltip card wrapper | | tooltipTitle | string | Title element | | tooltipDescription | string | Description element | | buttonNext | string | Next / Finish button | | buttonPrev | string | Back button | | buttonSkip | string | Skip button | | progress | string | "X of Y" progress indicator | | overlayColor | string | CSS color for the overlay background (default: rgba(0,0,0,0.55)) | | spotlightBorderRadius | number | Border radius (px) of the spotlight cutout (default: 4) |

Example — Tailwind overrides:

const tour = new Wayfinder({
  theme: {
    tooltip: 'bg-white rounded-xl shadow-lg p-4 max-w-sm',
    buttonNext: 'bg-indigo-600 text-white px-4 py-2 rounded-md hover:bg-indigo-700',
    buttonSkip: 'text-gray-500 hover:text-gray-700 text-sm',
  },
  steps: [...],
});

LauncherConfig

An optional self-serve beacon. A pulsing ring is rendered over the target element; clicking the element (or the ring) starts the tour and automatically removes the beacon.

| Property | Type | Required | Description | |---|---|---|---| | target | StepTarget | Yes | Element to attach the beacon to (CSS selector, HTMLElement, or resolver function). | | color | string | No | Color of the pulse ring and center dot. Default: '#4f46e5'. | | size | number | No | Diameter (px) of the center dot. Default: 14. |

Example — "Take a tour" help button:

const tour = new Wayfinder({
  launcher: {
    target: '[data-tour="help-btn"]',
    color: '#4f46e5',
    size: 16,
  },
  steps: [...],
});
// No need to call tour.start() — the beacon handles it.

EventHandlers

Hook into every user interaction to push data to your analytics tool. Every payload includes a flowId field — the ID of the matched Flow, or null when top-level steps are used.

| Event | Payload type | Fired when | |---|---|---| | onStart | { totalSteps: number, flowId: string \| null } | Tour begins | | onNext | { step: Step, index: number, flowId: string \| null } | User advances a step | | onPrev | { step: Step, index: number, flowId: string \| null } | User goes back a step | | onSkip | { step: Step, index: number, flowId: string \| null } | User dismisses the tour early | | onComplete | { steps: Step[], flowId: string \| null } | User finishes the last step | | onError | { step: Step \| null, error: string } | Target element not found in DOM |

Example — Mixpanel integration:

const tour = new Wayfinder({
  steps: [...],
  events: {
    onStart: ({ totalSteps }) => {
      mixpanel.track('Onboarding Started', { totalSteps });
    },
    onNext: ({ step }) => {
      mixpanel.track('Onboarding Step Viewed', { stepId: step.id });
    },
    onSkip: ({ step, index }) => {
      mixpanel.track('Onboarding Skipped', { dropoffStep: step.id, dropoffIndex: index });
    },
    onComplete: () => {
      mixpanel.track('Onboarding Completed');
    },
  },
});

Instance API

| Method | Description | |---|---| | start() | Start from step 0. No-ops if a tour is already running. | | next() | Advance to the next step. Fires onComplete on the last step. | | prev() | Go back one step. No-op on the first step. | | skip() | Dismiss the tour at the current step. Fires onSkip. | | goTo(index) | Jump to any step by zero-based index. | | destroy() | Tear down all DOM nodes and clear state. |

Static API

| Method | Returns | Description | |---|---|---| | Wayfinder.isRunning() | boolean | Returns true if any tour instance is currently active. Only one tour may run at a time. |

Example — guard against concurrent tours:

document.getElementById('start-tour')!.addEventListener('click', () => {
  if (Wayfinder.isRunning()) {
    console.warn('A tour is already in progress.');
    return;
  }
  tour.start();
});

Obfuscated class names

When your app uses CSS Modules, styled-components, Emotion, or a build minifier, class-based selectors become unstable across builds. Wayfinder provides three complementary strategies — pick the one that fits your stack.

Strategy 1 — tourId() helper (recommended, any framework)

Add a stable data-tour attribute to the element you want to target. Data attributes are never touched by any build tool.

import { Wayfinder, tourId } from '@wayfinder/core';
<!-- Mark the element once in your template/JSX -->
<button data-tour="create-btn">New Project</button>
// Reference it in your step config
{ id: 'create-btn', target: tourId('create-btn') }
// tourId('create-btn') returns '[data-tour="create-btn"]'

Use a custom attribute name if data-tour is taken:

new Wayfinder({
  tourAttribute: 'data-wf',   // changes the attribute tourId() produces
  steps: [{ target: tourId('create-btn', 'data-wf') }]
})

Strategy 2 — Resolver function target

Pass a lazy function as target. It is called fresh at each step transition:

{ target: () => document.querySelector('[data-testid="my-btn"]') }
{ target: () => myReactRef.current }

Strategy 3 — waitForTargetTimeoutMs (SPA async DOM)

Wayfinder always uses a MutationObserver to wait for target elements to appear in the DOM before showing each step — no extra flag required. This covers SPA route changes, modal opens, and panel reveals automatically.

Tune the timeout window with waitForTargetTimeoutMs (default 3000 ms). If the element doesn't appear in time, onError fires:

new Wayfinder({
  waitForTargetTimeoutMs: 5000,
  steps: [...],
})

Framework packages

| Package | How it helps | |---|---| | @wayfinder/react | useTourTarget hook + TourTarget component — writes data-tour via a React ref | | @wayfinder/vue | v-tour-target directive + WayfinderPlugin | | @wayfinder/vite-plugin | Injects data-tour at build time — zero markup changes needed |

Keyboard navigation

When a tour is active the following keyboard shortcuts are available globally:

| Key | Action | |---|---| | or Enter | Next step | | | Previous step | | Escape | Skip tour |

CSS customisation

Wayfinder injects a minimal stylesheet with the following class hooks. You can target these directly in your own CSS:

| Class | Element | |---|---| | .wf-overlay | Full-screen click-blocking overlay | | .wf-spotlight | The cutout div (box-shadow creates the dark surround) | | .wf-tooltip | Tooltip card wrapper | | .wf-tooltip-title | Title text | | .wf-tooltip-description | Description text | | .wf-tooltip-footer | Footer row (progress + controls) | | .wf-progress | "X of Y" text | | .wf-controls | Button container | | .wf-btn-next | Next / Finish button | | .wf-btn-prev | Back button | | .wf-btn-skip | Skip button |

Output formats

The package ships three build targets:

| File | Format | Use case | |---|---|---| | dist/wayfinder.js | ESM | Bundlers (Vite, Webpack, Rollup) | | dist/wayfinder.cjs | CommonJS | Node / legacy bundlers | | dist/wayfinder.umd.cjs | UMD | CDN / <script> tag | | dist/index.d.ts | TypeScript declarations | Type checking |

CDN usage:

<script src="https://unpkg.com/@floating-ui/dom"></script>
<script src="https://unpkg.com/@wayfinder/core/dist/wayfinder.umd.cjs"></script>
<script>
  const { Wayfinder } = Wayfinder; // UMD global
  const tour = new Wayfinder({ steps: [...] });
</script>

Development

# From the repo root
yarn install --no-immutable

# Build the library (outputs to packages/core/dist/)
yarn build

# Watch mode during development
yarn workspace @wayfinder/core run dev

# Run the interactive demo at http://localhost:3111
yarn dev

License

MIT