AI-Powered Product Tours. Guide users like you know them.
GuideFlow is a modular, framework-agnostic product tour library with a built-in finite state machine engine, AI-powered tour generation, analytics, and A/B testing. It works with React, Vue, Svelte, or plain JavaScript.
Documentation · Live Demo · npm
ExperimentEngine| Package | Description | Size |
|---|---|---|
@guideflow/core |
Zero-dependency FSM engine, spotlight, persistence, i18n | ~12 kB gzip |
@guideflow/react |
TourProvider, useTour, useTourStep, useHotspot |
— |
@guideflow/vue |
GuideFlowPlugin, useTour composable |
— |
@guideflow/svelte |
createTourStore, Readable stores |
— |
@guideflow/ai |
GuideBrain, OpenAI / Anthropic / Ollama providers |
— |
@guideflow/analytics |
AnalyticsCollector, transport adapters, ExperimentEngine |
— |
@guideflow/cli |
init, studio, export, push commands |
— |
@guideflow/devtools |
Browser extension for visual tour building (coming soon) | — |
Install the core engine and your framework adapter:
# pnpm
pnpm add @guideflow/core @guideflow/react
# npm
npm install @guideflow/core @guideflow/react
# yarn
yarn add @guideflow/core @guideflow/react
For AI features:
pnpm add @guideflow/ai openai # OpenAI
pnpm add @guideflow/ai @anthropic-ai/sdk # Anthropic
For analytics:
pnpm add @guideflow/analytics
import { createGuideFlow } from '@guideflow/core'
import '@guideflow/core/styles'
const gf = createGuideFlow()
gf.start({
id: 'welcome',
initial: 'intro',
states: {
intro: {
steps: [
{
id: 'step-1',
content: { title: 'Welcome!', body: 'This is your dashboard.' },
target: '#sidebar',
placement: 'right',
},
{
id: 'step-2',
content: { title: 'Your profile', body: 'Manage your account here.' },
target: '#profile-btn',
placement: 'bottom',
},
],
final: true,
},
},
})
import { createGuideFlow } from '@guideflow/core'
import { TourProvider, useTour } from '@guideflow/react'
import '@guideflow/core/styles'
const gf = createGuideFlow()
const welcomeFlow = gf.createFlow({
id: 'welcome',
initial: 'main',
states: {
main: {
steps: [
{
id: 'step-1',
content: { title: 'Hello!', body: 'Let us show you around.' },
target: '#hero',
placement: 'bottom',
},
],
final: true,
},
},
})
export function App() {
return (
<TourProvider instance={gf}>
<Dashboard />
</TourProvider>
)
}
function Dashboard() {
const { start, isActive, currentStepIndex, totalSteps } = useTour()
return (
<div>
<button onClick={() => start(welcomeFlow)}>Start Tour</button>
{isActive && <span>Step {currentStepIndex + 1} of {totalSteps}</span>}
</div>
)
}
// main.ts
import { createApp } from 'vue'
import { createGuideFlow } from '@guideflow/core'
import { GuideFlowPlugin } from '@guideflow/vue'
import '@guideflow/core/styles'
import App from './App.vue'
const gf = createGuideFlow()
const app = createApp(App)
app.use(GuideFlowPlugin, { instance: gf })
app.mount('#app')
<!-- OnboardingButton.vue -->
<script setup lang="ts">
import { useTour } from '@guideflow/vue'
const { start, isActive, currentStepIndex, totalSteps } = useTour()
const flow = {
id: 'welcome',
initial: 'main',
states: {
main: {
steps: [{ id: 'step-1', content: { title: 'Welcome!' }, target: '#hero', placement: 'bottom' }],
final: true,
},
},
}
</script>
<template>
<button @click="start(flow)">Start Tour</button>
<span v-if="isActive">Step {{ currentStepIndex + 1 }} of {{ totalSteps }}</span>
</template>
<script lang="ts">
import { createGuideFlow } from '@guideflow/core'
import { createTourStore } from '@guideflow/svelte'
import '@guideflow/core/styles'
const store = createTourStore(createGuideFlow())
const { isActive, currentStepIndex, totalSteps, start } = store
const flow = {
id: 'welcome',
initial: 'main',
states: {
main: {
steps: [{ id: 'step-1', content: { title: 'Hello!' }, target: '#hero', placement: 'bottom' }],
final: true,
},
},
}
</script>
<button on:click={() => start(flow)}>Start Tour</button>
{#if $isActive}
<span>Step {$currentStepIndex + 1} of {$totalSteps}</span>
{/if}
GuideFlow's AI layer lives in @guideflow/ai. All providers are lazy-loaded — only the SDK you actually
import is included in your bundle.
import { createGuideFlow } from '@guideflow/core'
import { createAI, OpenAIProvider } from '@guideflow/ai'
const gf = createGuideFlow()
createAI(
new OpenAIProvider({ apiKey: import.meta.env.VITE_OPENAI_KEY }),
gf,
)
// Generate steps from a natural-language description of the page
const steps = await gf.ai.generate('Walk me through the checkout flow')
await gf.start({ id: 'ai-tour', initial: 'main', states: { main: { steps, final: true } } })
import { AnthropicProvider } from '@guideflow/ai'
createAI(
new AnthropicProvider({ apiKey: import.meta.env.VITE_ANTHROPIC_KEY }),
gf,
)
import { OllamaProvider } from '@guideflow/ai'
createAI(new OllamaProvider({ model: 'llama3', baseUrl: 'http://localhost:11434' }), gf)
Passively watch user behaviour and trigger help flows automatically:
createAI(new OpenAIProvider({ apiKey: '...' }), gf, { autoWatch: false })
const stopWatch = gf.ai.watch()
gf.ai.on('intent:detected', (signal) => {
// signal.type: 'confused' | 'stuck' | 'exploring' | 'engaged'
if (signal.type === 'confused' && signal.confidence > 0.8) {
gf.start(helpFlow)
}
})
// Stop watching when no longer needed
stopWatch()
import { ConversationalPanel } from '@guideflow/react'
function HelpButton() {
const [open, setOpen] = useState(false)
return (
<>
<button onClick={() => setOpen(true)}>Help</button>
<ConversationalPanel open={open} onClose={() => setOpen(false)} />
</>
)
}
ConversationalPanelrequires@guideflow/aito be configured on the GuideFlow instance.
import { AnalyticsCollector, PostHogTransport, WebhookTransport } from '@guideflow/analytics'
const collector = new AnalyticsCollector({
userId: 'user-123',
globalProperties: { plan: 'pro', version: '2.1' },
})
collector
.addTransport(new PostHogTransport())
.addTransport(new WebhookTransport({ url: '/api/analytics/guideflow' }))
collector.attach(gf)
// Flush all buffered events (e.g. on page unload)
await collector.flush()
Events emitted:
| Event | Triggered when |
|---|---|
guideflow.tour.started |
Tour begins |
guideflow.tour.completed |
All steps finished |
guideflow.tour.abandoned |
Tour closed early |
guideflow.step.viewed |
Step enters viewport |
guideflow.step.exited |
Step dismissed (includes dwell_ms) |
guideflow.step.skipped |
Step conditionally skipped |
Available transports: PostHogTransport, MixpanelTransport, AmplitudeTransport, SegmentTransport, WebhookTransport
import { ExperimentEngine } from '@guideflow/analytics'
const engine = new ExperimentEngine('user-123')
const { value: theme } = engine.assign({
id: 'tour-theme-q1-2025',
variants: [
{ id: 'control', value: 'minimal', weight: 50 },
{ id: 'treatment', value: 'bold', weight: 50 },
],
})
// Assignment is deterministic — same userId always gets same variant
const gf = createGuideFlow({ /* theme */ })
GuideFlowConfig| Option | Type | Default | Description |
|---|---|---|---|
renderer |
RendererContract |
DefaultRenderer |
Custom step renderer |
persistence |
PersistenceConfig |
undefined |
Progress persistence settings |
context |
GuidanceContext |
{} |
Shared context passed to steps and guards |
spotlight |
SpotlightOptions |
{} |
Spotlight overlay options |
nonce |
string |
undefined |
CSP nonce for injected <style> tags |
injectStyles |
boolean |
true |
Auto-inject default CSS |
debug |
boolean |
false |
Enable debug logging |
SpotlightOptions| Option | Type | Default | Description |
|---|---|---|---|
padding |
number |
8 |
Padding around highlighted element (px) |
borderRadius |
number |
4 |
Corner radius of spotlight cutout (px) |
animated |
boolean |
true |
Animate spotlight transitions |
overlayColor |
string |
'#000' |
Overlay background color |
overlayOpacity |
number |
0.5 |
Overlay opacity (0–1) |
PersistenceConfig| Option | Type | Default | Description |
|---|---|---|---|
driver |
'localStorage' | 'indexedDB' | PersistenceDriver |
'localStorage' |
Storage backend |
key |
(userId: string) => string |
Built-in | Custom storage key factory |
ttl |
number |
2592000000 (30 days) |
Progress expiry in milliseconds |
GuidanceContext| Field | Type | Description |
|---|---|---|
userId |
string |
Used for persistence and analytics |
roles |
string[] |
Used in showIf guards |
featureFlags |
Record<string, boolean> |
Used in showIf guards |
[key] |
unknown |
Any additional custom data |
Step| Field | Type | Description |
|---|---|---|
id |
string |
Unique step identifier |
target |
string | Element | null |
CSS selector or DOM element to anchor to |
content |
StepContent | () => StepContent |
{ title?, body?, html? } — can be async |
placement |
PopoverPlacement |
One of 13 placements or 'center' |
showIf |
(ctx: TContext) => boolean |
Conditionally skip this step |
clickThrough |
boolean |
Allow clicks to pass through the spotlight |
scrollIntoView |
boolean |
Auto-scroll target into view (default true) |
actions |
StepAction[] |
Override default next/prev/skip buttons |
PopoverPlacement values: top, top-start, top-end, bottom, bottom-start, bottom-end, left, left-start, left-end, right, right-start, right-end, center
GuideFlow tours are finite state machines. Each state holds an array of steps; events trigger transitions.
import { createGuideFlow } from '@guideflow/core'
const gf = createGuideFlow({
context: { userId: 'u1', roles: ['admin'] },
})
const onboardingFlow = gf.createFlow({
id: 'onboarding',
initial: 'setup',
context: { completedSteps: 0 },
states: {
setup: {
steps: [
{ id: 'profile', content: { title: 'Set up your profile' }, target: '#profile-form' },
{ id: 'avatar', content: { title: 'Add a photo' }, target: '#avatar-upload' },
],
on: { NEXT: 'features' },
onExit: (ctx) => { ctx.completedSteps++ },
},
features: {
steps: [
{
id: 'dashboard',
content: { title: 'Your dashboard' },
target: '#dashboard',
// Only show to admins
showIf: (ctx) => ctx.roles?.includes('admin') ?? false,
},
],
final: true,
},
},
})
await gf.start(onboardingFlow)
Hotspots and hints persist independently of any active tour.
// Persistent pulsing beacon on an element
const id = gf.hotspot('#new-feature-btn', {
title: 'New!',
body: 'Check out the new export feature.',
placement: 'top',
color: '#6366f1',
})
// Remove later
gf.removeHotspot(id)
// Hint badges
gf.hints([
{ id: 'hint-1', target: '#settings', hint: 'Configure your preferences here' },
{ id: 'hint-2', target: '#export-btn', hint: 'Export your data as CSV' },
])
gf.showHints()
gf.hideHints()
GuideFlowInstance is an event emitter. Subscribe to any tour lifecycle event:
gf.on('tour:start', ({ flowId }) => console.warn('Tour started:', flowId))
gf.on('tour:complete', ({ flowId }) => console.warn('Tour completed:', flowId))
gf.on('tour:abandon', ({ flowId, stepId, stepIndex }) => { /* ... */ })
gf.on('step:enter', ({ stepId, stepIndex, target }) => { /* ... */ })
gf.on('step:exit', ({ stepId, stepIndex }) => { /* ... */ })
gf.on('step:skip', ({ stepId }) => { /* ... */ })
gf.on('hotspot:open', ({ id }) => { /* ... */ })
gf.on('hint:click', ({ id }) => { /* ... */ })
// All .on() calls return an unsubscribe function
const off = gf.on('tour:complete', handler)
off() // unsubscribe
Install the CLI globally or use it via pnpm exec:
pnpm add -g @guideflow/cli
| Command | Description |
|---|---|
guideflow init |
Scaffold a guideflow.config.ts and example flow |
guideflow studio |
Launch the visual tour editor (opens browser) |
guideflow export |
Export flow definitions to JSON |
guideflow push |
Publish flows to GuideFlow Cloud |
GuideFlow includes a compatibility layer for attribute-based tours:
<!-- Intro.js-style attributes are supported -->
<div data-intro="Welcome to the dashboard" data-step="1" data-position="right">...</div>
import { autoInit } from '@guideflow/core'
// Automatically scans data-intro attributes and starts a tour
autoInit()
npm install -g pnpm)git clone https://github.com/RealNerdZW/GuideFlow.git
cd GuideFlow
pnpm install
pnpm build
| Script | Description |
|---|---|
pnpm dev |
Start all packages in watch mode |
pnpm build |
Build all packages |
pnpm test |
Run unit tests (Vitest) |
pnpm test:e2e |
Run Playwright end-to-end tests |
pnpm lint |
Lint with ESLint |
pnpm type-check |
Run TypeScript type-checking |
pnpm storybook |
Launch Storybook component explorer |
pnpm docs:dev |
Start the VitePress documentation site |
pnpm docs:build |
Build the documentation site |
pnpm size |
Check bundle sizes against limits |
pnpm clean |
Remove all build artifacts |
GuideFlow uses Changesets for versioning:
# 1. Add a changeset describing your change
pnpm changeset
# 2. Bump versions
pnpm version-packages
# 3. Publish to npm
pnpm publish-packages
MIT © 2026 GuideFlow Contributors