Skip to content

CMolG/onboarding-tools

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

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors