reguide
v0.5.0
Published
A React library for guided tours
Readme
reguide
Guided tours for React apps with spotlight targeting, step-by-step flow control, and configurable UI.
What You Get
- Spotlight highlight around the active target element
- Floating card with Back, Next, and Close controls
- Step progression modes (
default,click,interact,custom) - Optional section steps (multi-step groups under one top-level title)
- Optional progress persistence by key
- Simple imperative API through
useReguide
Requirements
- React
^18.0.0or^19.0.0 - React DOM
^18.0.0or^19.0.0
Install
pnpm add reguide react react-domImport styles once, usually in your app entry file:
import 'reguide/style.css'Quick Start
This example wires a 3-step tour with typed step ids and includes Start/Stop controls.
import { createReguideContext, type ReguideStep } from 'reguide'
import 'reguide/style.css'
const STEP_IDS = {
PROFILE: 'PROFILE',
SEARCH: 'SEARCH',
PUBLISH: 'PUBLISH',
} as const
type StepId = keyof typeof STEP_IDS
const reguide = createReguideContext<StepId>()
const { Provider, useReguide, useTarget } = reguide
function TourControls() {
const start = useReguide((state) => state.start)
const prev = useReguide((state) => state.prev)
const next = useReguide((state) => state.next)
const stop = useReguide((state) => state.stop)
const canGoPrev = useReguide((state) => state.canGoPrev)
const canGoNext = useReguide((state) => state.canGoNext)
return (
<div style={{ display: 'flex', gap: 8 }}>
<button type='button' onClick={start}>Start tour</button>
<button type='button' onClick={prev} disabled={!canGoPrev}>Back</button>
<button type='button' onClick={next} disabled={!canGoNext}>Next</button>
<button type='button' onClick={stop}>Close</button>
</div>
)
}
function ProfileButton() {
const ref = useTarget<HTMLButtonElement>(STEP_IDS.PROFILE)
return <button ref={ref} type='button'>Profile</button>
}
function SearchInput() {
const ref = useTarget<HTMLInputElement>(STEP_IDS.SEARCH)
return <input ref={ref} aria-label='Search' />
}
function PublishButton() {
const ref = useTarget<HTMLButtonElement>(STEP_IDS.PUBLISH)
return <button ref={ref} type='button'>Publish</button>
}
export function App() {
const steps: ReguideStep<StepId>[] = [
{
id: STEP_IDS.PROFILE,
title: 'Open your profile menu',
body: 'Click this button to continue.',
mode: 'click',
},
{
id: STEP_IDS.SEARCH,
title: 'Try search',
body: 'Type in the input to enable Next.',
mode: 'interact',
autoFocus: true,
},
{
id: STEP_IDS.PUBLISH,
title: 'Publish your first item',
body: 'Final step. Click Next to close.',
},
]
return (
<Provider steps={steps}>
<ProfileButton />
<SearchInput />
<PublishButton />
<TourControls />
</Provider>
)
}Core Concepts
Step Types
ReguideStep can be one of these:
- Standard step:
title,body, optionaltargetReforid - Custom step: standard step +
mode: 'custom'+validator - Section step: top-level
title+steps(children usesubtitle+body)
Progression Modes
default: Next is enabled immediately.click: Clicking the target advances to the next step.interact: Next stays disabled until click/input/keydown occurs on the target.custom: Next stays disabled until your validator returnstrue.
Custom validator example:
const hasSavedRef = useRef(false)
const saveButtonRef = useRef<HTMLButtonElement | null>(null)
const steps: ReguideStep[] = [
{
title: 'Save your settings',
body: 'Click Save to continue.',
targetRef: saveButtonRef,
mode: 'custom',
progressOnValidate: true,
validator: ({ eventType }) => eventType === 'click' && hasSavedRef.current,
},
]
<button
ref={saveButtonRef}
type='button'
onClick={() => {
hasSavedRef.current = true
}}
>
Save
</button>Section Steps
Use section steps when a top-level stage has multiple sub-steps.
const dataSourceRef = useRef<HTMLButtonElement | null>(null)
const reportNameRef = useRef<HTMLInputElement | null>(null)
const steps: ReguideStep[] = [
{
title: 'Create your first report',
steps: [
{
subtitle: 'Pick a data source',
body: 'Select any source to continue.',
targetRef: dataSourceRef,
mode: 'interact',
},
{
subtitle: 'Name the report',
body: 'Use a clear name so teammates can find it.',
targetRef: reportNameRef,
},
],
},
]In a section, the UI shows a Skip section action. It is disabled when there is no following top-level step.
Target Binding
Option 1: Direct refs on steps
Pass targetRef directly in each step. This is the simplest approach.
Option 2: Step ids + target registration
Use id on steps and register targets independently. This helps when step definitions live in a different module from UI components.
Factory example (define once, export, and consume in app components):
// reguide-targets.ts
import { createReguideContext } from 'reguide'
type StepId = 'profile' | 'search'
export const reguide = createReguideContext<StepId>()// App.tsx
import { type ReguideStep } from 'reguide'
import { reguide } from './reguide-targets'
const { Provider, useTarget } = reguide
function ProfileButton() {
const ref = useTarget<HTMLButtonElement>('profile')
return <button ref={ref} type='button'>Profile</button>
}
function SearchInput() {
const ref = useTarget<HTMLInputElement>('search')
return <input ref={ref} aria-label='Search' />
}
const steps: ReguideStep<'profile' | 'search'>[] = [
{
id: 'profile',
title: 'Open profile menu',
body: 'Click to continue.',
mode: 'click',
},
{
id: 'search',
title: 'Use search',
body: 'Type any text.',
mode: 'interact',
},
]
export function App() {
return (
<Provider steps={steps}>
<ProfileButton />
<SearchInput />
</Provider>
)
}If both targetRef and id registration are available for a step, targetRef wins.
ReguideProvider Reference
<ReguideProvider steps={steps} initialOpen={false}>
{children}
</ReguideProvider>Props:
steps: ReguideStep[](required)initialOpen?: booleantheme?: ReguideThemebuttonText?: ReguideButtonTextpersistence?: ReguidePersistenceOptionsonStart?: () => void | Promise<void>onStop?: (event: ReguideStopEvent) => void | Promise<void>onStepChange?: (event: ReguideStepChangeEvent) => void | Promise<void>
Persistence
Persist progress and restore it later:
<ReguideProvider
steps={steps}
persistence={{
key: 'my-app:onboarding',
persistIsOpen: true,
}}
>
{children}
</ReguideProvider>The persisted state stores step id when available, with step index fallback.
Lifecycle callbacks
<ReguideProvider
steps={steps}
onStart={() => track('guide_started')}
onStop={(event) => track('guide_stopped', event)}
onStepChange={(event) => track('guide_step_changed', event)}
>
{children}
</ReguideProvider>onStepChange includes:
source(start,next,prev,goToStep,goToStepById,skipSection,click,custom-auto,restore)currentStepIndex,currentStepIdpreviousStepIndex,previousStepId
useReguide Reference
useReguide() returns:
isOpenstepscurrentStepIndex(top-level step index)currentLeafStepIndex(rendered step index, including section children)currentStepinteractionSatisfiedcanGoNextcanGoPrevstart(): Promise<void>stop(): Promise<void>next(): Promise<void>prev(): Promise<void>goToStep(index: number): Promise<void>goToStepById(id: string): Promise<void>
useReguide(selector) subscribes only to the selected slice. Use this for action-only and read-only components that should avoid rerendering on unrelated guide updates.
Notes
useReguidemust be used insideReguideProvider.- Pressing Escape closes the guide.
- On the last step, Next is replaced with Close.
Theme Customization
Set defaults on the provider and override specific steps when needed.
<ReguideProvider
steps={steps}
theme={{
backdrop: { color: '#020617', opacity: 0.7 },
card: {
background: '#ffffff',
border: '1px solid #d6dce7',
padding: 20,
verticalOffset: 16,
className: 'my-guide-card',
style: { maxWidth: 420 },
},
title: { fontWeight: 700, color: '#0f172a' },
body: { color: '#334155' },
highlight: { borderRadius: 16, padding: 10 },
stepCount: { show: true },
buttons: {
secondary: {
background: '#f8fafc',
border: '1px solid #cbd5e1',
color: '#0f172a',
},
primary: {
background: '#0f172a',
border: '1px solid #0f172a',
color: '#ffffff',
},
},
}}
>
{children}
</ReguideProvider>Integration Checklist
Use this when the guide does not behave as expected.
| Issue | Verify | Resolution |
| --- | --- | --- |
| Error: useReguide must be used within a ReguideProvider | The component calling useReguide() is rendered under ReguideProvider. | Move that component inside ReguideProvider in the same React tree. |
| Guide opens, but there is no spotlight cutout | The current step resolves a target via targetRef or step id registration, and the target element is mounted. | If no target resolves, reguide intentionally renders a centered card with a full backdrop. Add or fix target binding to get a cutout spotlight. |
| goToStepById('...') does nothing | The id exists in steps and is unique. | Add the missing id, or rename duplicate ids. Unknown ids are ignored, and duplicate ids resolve to the last match. |
| guide.start() is called but no card appears | steps contains at least one renderable step. | Provide at least one step. If there is no current step, the guide state can open without rendering a card. |
| Guide renders, but default styles are missing | import 'reguide/style.css' is present in app startup code. | Add the stylesheet import once in your app entry path, for example main.tsx. |
