Skip to content

Repository files navigation

                                                                          
   ██████╗ ███╗   ██╗██████╗  ██████╗  █████╗ ██████╗ ██████╗ ██╗███╗   ██╗ ██████╗ 
  ██╔═══██╗████╗  ██║██╔══██╗██╔═══██╗██╔══██╗██╔══██╗██╔══██╗██║████╗  ██║██╔════╝ 
  ██║   ██║██╔██╗ ██║██████╔╝██║   ██║███████║██████╔╝██║  ██║██║██╔██╗ ██║██║  ███╗
  ██║   ██║██║╚██╗██║██╔══██╗██║   ██║██╔══██║██╔══██╗██║  ██║██║██║╚██╗██║██║   ██║
  ╚██████╔╝██║ ╚████║██████╔╝╚██████╔╝██║  ██║██║  ██║██████╔╝██║██║ ╚████║╚██████╔╝
   ╚═════╝ ╚═╝  ╚═══╝╚═════╝  ╚═════╝ ╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝ ╚═╝╚═╝  ╚═══╝ ╚═════╝ 

                  ████████╗ ██████╗  ██████╗ ██╗     ███████╗
                  ╚══██╔══╝██╔═══██╗██╔═══██╗██║     ██╔════╝
                     ██║   ██║   ██║██║   ██║██║     ███████╗
                     ██║   ██║   ██║██║   ██║██║     ╚════██║
                     ██║   ╚██████╔╝╚██████╔╝███████╗███████║
                     ╚═╝    ╚═════╝  ╚═════╝ ╚══════╝╚══════╝

Declarative unlockable onboarding, progressive disclosure, and tutorial overlays for any React app.

Tag a component as Unlockable. Describe when it should appear.
The state machine, persistence, animations, and tutorial overlay are handled for you.

npm license types zero deps react peer bundle size

Quickstart · Examples · Concepts · Theming · Frameworks · API · For AI agents · Contributing


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▄░█ █▀█    ▀▄▀ █░█ █▄█    ▀█▀ █░█ █ █▀                        ║
║    █░▀█ █▄█    █░█ █▀█ ░█░    ░█░ █▀█ █ ▄█    E X I S T S         ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

Most onboarding libraries assume a linear product tour: step 1, step 2, step 3, done. Real products are not linear. Features should appear when the user is ready for them — after a personality test, after they ship their first artifact, after an AI clusters them into an archetype, or after a feature flag flips.

onboarding-tools flips the model:

- A separate "tour" component that points at things                
- A scripted, ordered list of steps                                 
- Hard-coded conditions baked into your routes                      

+ Each feature declares **its own** unlock criteria                 
+ Components stay hidden until those criteria are met               
+ A state machine handles the transition, animation, and overlay    
+ AI / archetype / flag-driven personalisation is a first-class API 

It is the React equivalent of slapping a @Unlockable("builder") annotation on a Java component and letting the runtime decide who sees it.


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀▀ █▀▀ ▄▀█ ▀█▀ █░█ █▀█ █▀▀ █▀                                 ║
║    █▀░ ██▄ █▀█ ░█░ █▄█ █▀▄ ██▄ ▄█                                 ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝
  • Zero runtime dependencies. Only React as an optional peer. No react-router, no framer-motion, no clsx, no Vite globals.
  • Framework-agnostic. Vite, Next.js, Remix, CRA, Webpack — anything that runs React. Router and storage are pluggable adapters.
  • Type-safe by default. Strict TypeScript. Public types live in a single core/types.ts.
  • Three install surfaces. Use /core standalone (pure logic, even from Vue/Svelte), /react for the components, /testing for unit tests.
  • Persistent state machine. HIDDEN → ELIGIBLE → UNLOCKING → UNLOCKED with a serializable, append-only localStorage log.
  • Declarative criteria DSL. Combine event, archetype, flag, state, unlockable, resolver with all / any / not.
  • AI-ready resolver hook. Plug in any function (local heuristic, remote LLM, classifier) that returns UnlockDecision[].
  • Tutorial overlay. Spotlight + coach card with reduced-motion support, fallback positioning, and full theming via CSS variables.
  • Flow derivation. Topological sort with cycle detection so you know what stage the user is on without writing a state diagram.

╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █ █▄░█ █▀ ▀█▀ ▄▀█ █░░ █░░                                      ║
║    █ █░▀█ ▄█ ░█░ █▀█ █▄▄ █▄▄                                      ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝
npm install onboarding-tools
# or
pnpm add onboarding-tools
# or
yarn add onboarding-tools

Import the default styles once in your app root (e.g. main.tsx / _app.tsx / layout.tsx):

import 'onboarding-tools/styles.css';

Override CSS custom properties to re-skin without forking — see Theming.


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀█ █░█ █ █▀▀ █▄▀ █▀ ▀█▀ ▄▀█ █▀█ ▀█▀                           ║
║    ▀▀█ █▄█ █ █▄▄ █░█ ▄█ ░█░ █▀█ █▀▄ ░█░                           ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

Define your unlockable catalog:

import 'onboarding-tools/styles.css';
import type { UnlockableDefinition } from 'onboarding-tools';
import {
  Unlockable,
  UnlockableCatalogRegistrar,
  UnlockableFlowProvider,
  UnlockableProvider,
  UnlockableTutorialEngineProvider,
} from 'onboarding-tools/react';

const definitions: UnlockableDefinition[] = [
  {
    id: 'profile',
    activation: 'automatic',
    meta: { title: 'Profile', description: 'Complete your profile first.' },
    flow: { stage: 'Profile', order: 10, completionEvent: 'profile.completed' },
  },
  {
    id: 'dashboard',
    activation: 'manual',
    visibility: 'hidden',
    meta: { title: 'Dashboard', description: 'Unlock the dashboard after setup.' },
    unlocksOn: { kind: 'event', event: 'profile.completed' },
    flow: { stage: 'Dashboard', order: 20, completionEvent: 'dashboard.opened' },
  },
];

export function App() {
  return (
    <UnlockableProvider appId="demo">
      <UnlockableCatalogRegistrar definitions={definitions} />
      <UnlockableFlowProvider>
        <UnlockableTutorialEngineProvider>
          <Unlockable definition={definitions[1]}>
            <Dashboard />
          </Unlockable>
        </UnlockableTutorialEngineProvider>
      </UnlockableFlowProvider>
    </UnlockableProvider>
  );
}

That's the whole API surface for a basic setup. The Dashboard component stays out of the DOM until emitEvent('profile.completed') fires; then the overlay confirms the unlock, plays the reveal animation, and the tutorial engine narrates the new capability.


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀▀ ▀▄▀ ▄▀█ █▀▄▀█ █▀█ █░░ █▀▀ █▀                               ║
║    ██▄ █░█ █▀█ █░▀░█ █▀▀ █▄▄ ██▄ ▄█                               ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

A runnable Next.js (App Router) demo lives in examples/next. It wires the three main patterns — automatic, event-gated, and archetype-gated — and is also the easiest way to see the unlock animation, the manual confirmation overlay, and the tutorial engine in action.

Deploy with Vercel

The deploy button targets examples/next as the project root. The example's vercel.json builds the parent onboarding-tools package before next build, so no extra setup is required.

Run it locally:

cd examples/next
npm install
npm run dev

The example links the package via file:../.. and runs npm run build on the parent through a predev / prebuild hook, so any edit in src/ lands in the demo on the next dev-server start.

Why no Vite example? The only difference between Vite and Next.js here is the router adapter (~5 lines) and the 'use client' boundary. Maintaining two near-identical demos was redundant — the router-adapter pattern is documented under Router adapter for both stacks.


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀▀ █▀█ █▄░█ █▀▀ █▀▀ █▀█ ▀█▀ █▀                                ║
║    █▄▄ █▄█ █░▀█ █▄▄ ██▄ █▀▀ ░█░ ▄█                                ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

The state machine

  ┌──────────┐   criteria met   ┌────────────┐  user/auto   ┌────────────┐  effect done   ┌────────────┐
  │  HIDDEN  │ ───────────────▶ │  ELIGIBLE  │ ───────────▶ │ UNLOCKING  │ ─────────────▶ │  UNLOCKED  │
  └──────────┘                  └────────────┘              └────────────┘                └────────────┘
  • automatic activation jumps straight from ELIGIBLE to UNLOCKING.
  • manual activation waits for confirmUnlock(id) (overlay button or tutorial step).
  • UNLOCKED is the terminal state. It is persisted to your storage adapter and survives reloads.

Criteria DSL

Every unlocksOn is a tree of criteria, combined with all / any / not:

{
  unlocksOn: {
    all: [
      { kind: 'event', event: 'onboarding.completed' },
      { any: [
        { kind: 'archetype', value: 'builder' },
        { kind: 'flag', key: 'beta.enabled', value: true },
      ]},
      { not: { kind: 'unlockable', id: 'legacy-flow', status: 'UNLOCKED' } },
    ],
  },
}

Available criteria kinds: event, archetype, flag, state, unlockable, resolver.

Archetypes (a.k.a. hyper-personalisation)

Tag a definition with one or more archetypes and mark it autoAssignable:

{
  id: 'cv-coach',
  archetype: ['builder', 'storyteller'],
  autoAssignable: true,
  meta: { title: 'CV Coach', description: '…', tags: ['cv-management'] },
}

After your AI / personality test resolves a user, push the result into the provider:

const { setUserArchetypes, addSignal } = useUnlockableSignals();
setUserArchetypes(['builder']);   // from clustering output
addSignal('cv-management');        // from a feature-interest signal

The built-in createLocalUnlockableResolver matches archetypes against meta.tags, meta.capability, and meta.audience. Replace it with any custom UnlockResolver (e.g. an LLM call) by passing resolver to UnlockableProvider.

Flow

When definitions declare flow.stage / flow.order / flow.completionEvent, UnlockableFlowProvider derives a topologically-sorted onboarding graph with cycle detection. Use useUnlockableFlow() to render progress UI or gate routes with <UnlockableFlowRouteGate />.

Tutorial overlay

UnlockableTutorialEngineProvider mounts a single coach overlay that:

  • Spotlights a CSS selector (tutorial.steps[].target).
  • Falls back to a centered card if the target is not on screen yet.
  • Respects prefers-reduced-motion.
  • Supports multi-step tutorials with next / confirmUnlock / focusTarget / clickTarget actions.

╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀█ █▀█ █░█ ▀█▀ █▀▀ █▀█    ▄▀█ █▀▄ ▄▀█ █▀█ ▀█▀ █▀▀ █▀█         ║
║    █▀▄ █▄█ █▄█ ░█░ ██▄ █▀▄    █▀█ █▄▀ █▀█ █▀▀ ░█░ ██▄ █▀▄         ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

onboarding-tools does not bundle a router. Pass a tiny adapter to plug in the one you already use:

// React Router v6
import { useLocation, useNavigate } from 'react-router-dom';
import type { OnboardingRouterAdapter } from 'onboarding-tools/react';

function useOnboardingRouter(): OnboardingRouterAdapter {
  const location = useLocation();
  const navigate = useNavigate();
  return {
    pathname: location.pathname,
    navigate: (path, options) => navigate(path, { replace: options?.replace }),
  };
}
// Next.js App Router
'use client';
import { usePathname, useRouter } from 'next/navigation';
import type { OnboardingRouterAdapter } from 'onboarding-tools/react';

function useOnboardingRouter(): OnboardingRouterAdapter {
  const pathname = usePathname() ?? '/';
  const router = useRouter();
  return {
    pathname,
    navigate: (path, options) =>
      options?.replace ? router.replace(path) : router.push(path),
  };
}

Pass the adapter to UnlockableFlowProvider and UnlockableTutorialEngineProvider.


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀ ▀█▀ █▀█ █▀█ ▄▀█ █▀▀ █▀▀    ▄▀█ █▀▄ ▄▀█ █▀█ ▀█▀ █▀▀ █▀█      ║
║    ▄█ ░█░ █▄█ █▀▄ █▀█ █▄█ ██▄    █▀█ █▄▀ █▀█ █▀▀ ░█░ ██▄ █▀▄      ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

The default storage uses window.localStorage and is SSR-safe (gracefully no-ops when window is undefined). Swap it for anything that satisfies UnlockableStorageAdapter:

<UnlockableProvider
  appId="demo"
  storage={{
    getItem: (key) => sessionStorage.getItem(key),
    setItem: (key, value) => sessionStorage.setItem(key, value),
    removeItem: (key) => sessionStorage.removeItem(key),
  }}
/>

For tests, import the in-memory adapter:

import { createMemoryStorage } from 'onboarding-tools/testing';

const storage = createMemoryStorage();

╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    ▀█▀ █░█ █▀▀ █▀▄▀█ █ █▄░█ █▀▀                                   ║
║    ░█░ █▀█ ██▄ █░▀░█ █ █░▀█ █▄█                                   ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

Override CSS custom properties anywhere in your stylesheet — no Sass, no runtime theme provider:

:root {
  --ot-color-primary: #0f766e;
  --ot-color-primary-soft: rgb(15 118 110 / 12%);
  --ot-color-backdrop: rgb(2 6 23 / 65%);
  --ot-radius-card: 20px;
  --ot-shadow-card: 0 30px 90px rgb(15 23 42 / 22%);
  --ot-z-overlay: 70;
  --ot-z-tutorial: 80;
}

Need deeper customisation? Pass a theme prop to UnlockableProvider:

<UnlockableProvider
  theme={{
    className: 'my-app-onboarding',
    tokens: { '--ot-color-primary': '#0f766e' },
    defaultEffect: { name: 'pulse', durationMs: 700 },
    overlay: { kind: 'spotlight', primaryActionLabel: 'Show me' },
  }}
/>

╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀▀ █▀█ ▄▀█ █▀▄▀█ █▀▀ █░█░█ █▀█ █▀█ █▄▀ █▀                     ║
║    █▀░ █▀▄ █▀█ █░▀░█ ██▄ ▀▄▀▄▀ █▄█ █▀▄ █░█ ▄█                     ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝
Host Status Notes
Vite + React Reference setup. Used in tests/ (jsdom).
Next.js (App Router) Wrap providers in a 'use client' boundary. SSR-safe storage.
Next.js (Pages Router) Same — providers go in _app.tsx.
Remix Mount providers in app/root.tsx inside the client tree.
Create React App Works as-is.
Webpack / Rspack ESM output; sideEffects declared for styles.css.
Bun / esbuild No CJS pitfalls.
React Native ⚠️ core/* works. The DOM overlay needs a custom renderer.

╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    ▄▀█ █▀█ █    █▀█ █▀▀ █▀▀ █▀▀ █▀█ █▀▀ █▄░█ █▀▀ █▀▀              ║
║    █▀█ █▀▀ █    █▀▄ ██▄ █▀░ ██▄ █▀▄ ██▄ █░▀█ █▄▄ ██▄              ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝
Surface Where it lives
Types (UnlockableDefinition, …) onboarding-tools (root)
Pure logic (state, criteria, flow, …) onboarding-tools/core
React bindings, hooks, components onboarding-tools/react
In-memory storage for tests onboarding-tools/testing
Stylesheet onboarding-tools/styles.css

Key exports from onboarding-tools/react:

Export Purpose
<UnlockableProvider> Root provider. Owns state, storage, theme.
<UnlockableCatalogRegistrar> Registers definitions on mount.
<UnlockableFlowProvider> Derives the onboarding flow graph.
<UnlockableFlowRouteGate> Gates a route by stage completion.
<UnlockableTutorialEngineProvider> Mounts the tutorial overlay engine.
<Unlockable> Wraps a component as an unlock target.
<UnlockableOverlay> Standalone unlock confirmation overlay.
useUnlockable(id) Status + control for a single unlockable.
useUnlockableCatalog() Serializable catalog (for AI / analytics).
useUnlockableSignals() Archetypes, signals, flags.
useUnlockableEvents() Emit / read domain events.
useUnlockableFlow() Stages, active stage, completion lookup.

Full type signatures live in src/core/types.ts (shipped as .d.ts).


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀█ █▀█ ▄▀█ █▀▄ █▀▄▀█ ▄▀█ █▀█                                  ║
║    █▀▄ █▄█ █▀█ █▄▀ █░▀░█ █▀█ █▀▀                                  ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝
  • React Native renderer for the tutorial overlay.
  • Devtools panel: visualise catalog, criteria, decisions live.
  • First-class adapter for TanStack Router and Wouter.
  • Headless overlay primitives so users can ship their own UI.
  • Built-in resolver helpers for OpenAI / Anthropic classification.
  • Storybook with every state and animation pinned.

Have an idea? Open a discussion or read CONTRIBUTING.md.


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █▀▀ █▀█ █▄░█ ▀█▀ █▀█ █ █▄▄ █░█ ▀█▀ █ █▄░█ █▀▀                  ║
║    █▄▄ █▄█ █░▀█ ░█░ █▀▄ █ █▄█ █▄█ ░█░ █ █░▀█ █▄█                  ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

PRs welcome. Read CONTRIBUTING.md for the local setup, conventions, and release process. Be kind — we follow the Contributor Covenant.

Security issues: see SECURITY.md.


╔═══════════════════════════════════════════════════════════════════╗
║                                                                   ║
║    █░░ █ █▀▀ █▀▀ █▄░█ █▀ █▀▀                                      ║
║    █▄▄ █ █▄▄ ██▄ █░▀█ ▄█ ██▄                                      ║
║                                                                   ║
╚═══════════════════════════════════════════════════════════════════╝

MIT © 2026 Carlos and onboarding-tools contributors.

Built for products that grow with the user, not against them.

About

No description, website, or topics provided.

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages