flowsterix-best-practices
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFlowsterix Best Practices
Flowsterix 最佳实践
Flowsterix is a state machine-based guided tour library for React applications. Flows are declarative step sequences with automatic progression rules, lifecycle hooks, and persistence.
Flowsterix 是一个基于状态机的React应用引导流程库。引导流是声明式的步骤序列,具备自动推进规则、生命周期钩子和状态持久化能力。
Quick Start
快速开始
Installation
安装
bash
undefinedbash
undefinedCore packages
核心包
npm install @flowsterix/core @flowsterix/react motion
npm install @flowsterix/core @flowsterix/react motion
Recommended: Add preconfigured shadcn components
推荐:添加预配置的shadcn组件
npx shadcn@latest add https://flowsterix.com/r/tour-hud.json
**Prefer the shadcn components** - they provide polished, accessible UI out of the box and follow the design patterns shown in the examples.npx shadcn@latest add https://flowsterix.com/r/tour-hud.json
**优先使用shadcn组件** - 它们提供了开箱即用的精致、可访问的UI,并且遵循示例中展示的设计模式。Minimal Example
最简示例
tsx
import { createFlow, type FlowDefinition } from '@flowsterix/core'
import { TourProvider, TourHUD } from '@flowsterix/react'
import type { ReactNode } from 'react'
const onboardingFlow: FlowDefinition<ReactNode> = createFlow({
id: 'onboarding',
version: { major: 1, minor: 0 },
autoStart: true,
steps: [
{
id: 'welcome',
target: 'screen',
advance: [{ type: 'manual' }],
content: <p>Welcome to our app!</p>,
},
{
id: 'feature',
target: { selector: '[data-tour-target="main-feature"]' },
advance: [{ type: 'event', event: 'click', on: 'target' }],
content: <p>Click this button to continue</p>,
},
],
})
export function App({ children }) {
return (
<TourProvider flows={[onboardingFlow]} storageNamespace="my-app">
<TourHUD overlay={{ showRing: true }} />
{children}
</TourProvider>
)
}tsx
import { createFlow, type FlowDefinition } from '@flowsterix/core'
import { TourProvider, TourHUD } from '@flowsterix/react'
import type { ReactNode } from 'react'
const onboardingFlow: FlowDefinition<ReactNode> = createFlow({
id: 'onboarding',
version: { major: 1, minor: 0 },
autoStart: true,
steps: [
{
id: 'welcome',
target: 'screen',
advance: [{ type: 'manual' }],
content: <p>欢迎使用我们的应用!</p>,
},
{
id: 'feature',
target: { selector: '[data-tour-target="main-feature"]' },
advance: [{ type: 'event', event: 'click', on: 'target' }],
content: <p>点击此按钮继续</p>,
},
],
})
export function App({ children }) {
return (
<TourProvider flows={[onboardingFlow]} storageNamespace="my-app">
<TourHUD overlay={{ showRing: true }} />
{children}
</TourProvider>
)
}Core Concepts
核心概念
FlowDefinition
FlowDefinition
tsx
createFlow({
id: string, // Unique identifier
version: { major: number, minor: number }, // For storage migrations
steps: Step[], // Array of tour steps
dialogs?: Record<string, DialogConfig>, // Dialog configurations (see Radix Dialog Integration)
autoStart?: boolean, // Start on mount (default: false)
resumeStrategy?: 'chain' | 'current', // How to run onResume hooks
hud?: FlowHudOptions, // UI configuration
migrate?: (ctx) => FlowState | null, // Version migration handler
})tsx
createFlow({
id: string, // 唯一标识符
version: { major: number, minor: number }, // 用于存储迁移
steps: Step[], // 引导步骤数组
dialogs?: Record<string, DialogConfig>, // 对话框配置(参见Radix Dialog集成)
autoStart?: boolean, // 挂载时启动(默认:false)
resumeStrategy?: 'chain' | 'current', // onResume钩子的执行方式
hud?: FlowHudOptions, // UI配置
migrate?: (ctx) => FlowState | null, // 版本迁移处理函数
})Step Anatomy
步骤结构
tsx
{
id: string, // Unique within flow
target: StepTarget, // What to highlight
content: ReactNode, // Popover content
dialogId?: string, // Reference to flow.dialogs entry (auto-opens dialog)
advance?: AdvanceRule[], // When to move to next step
placement?: StepPlacement, // Popover position
route?: string | RegExp, // Only show on matching routes
waitFor?: StepWaitFor, // Block until condition met
targetBehavior?: StepTargetBehavior, // Scroll/visibility handling
onEnter?: (ctx) => void, // Fires when step activates
onResume?: (ctx) => void, // Fires when resuming from storage
onExit?: (ctx) => void, // Fires when leaving step
controls?: { back?, next? }, // Button visibility
}tsx
{
id: string, // 在当前流内唯一
target: StepTarget, // 要高亮的目标
content: ReactNode, // 弹出框内容
dialogId?: string, // 关联flow.dialogs中的配置项(自动打开对话框)
advance?: AdvanceRule[], // 推进到下一步的触发规则
placement?: StepPlacement, // 弹出框位置
route?: string | RegExp, // 仅在匹配的路由上显示
waitFor?: StepWaitFor, // 满足条件前阻塞步骤
targetBehavior?: StepTargetBehavior, // 滚动/可见性处理
onEnter?: (ctx) => void, // 步骤激活时触发
onResume?: (ctx) => void, // 从存储恢复时触发
onExit?: (ctx) => void, // 离开步骤时触发
controls?: { back?, next? }, // 按钮可见性配置
}Step Targets
步骤目标
tsx
// Full-screen overlay (no element highlight)
target: 'screen'
// CSS selector (recommended: use data attributes)
target: {
selector: '[data-tour-target="feature"]'
}
// Dynamic node resolution
target: {
getNode: () => document.querySelector('.dynamic-el')
}Always use attributes instead of CSS classes for stability.
data-tour-targettsx
// 全屏遮罩(无元素高亮)
target: 'screen'
// CSS选择器(推荐:使用data属性)
target: {
selector: '[data-tour-target="feature"]'
}
// 动态节点解析
target: {
getNode: () => document.querySelector('.dynamic-el')
}始终使用属性而非CSS类,以保证稳定性。
data-tour-targetAdvance Rules
推进规则
Rules define when a step automatically progresses. First matching rule wins.
| Type | Usage | Example |
|---|---|---|
| Next button only | |
| DOM event on target | |
| Timer-based | |
| URL change | |
| Polling condition | |
tsx
// Combine rules for flexibility
advance: [
{ type: 'event', event: 'click', on: 'target' },
{ type: 'delay', ms: 10000 }, // Fallback after 10s
]规则定义步骤自动推进的时机,匹配到的第一条规则生效。
| 类型 | 用法 | 示例 |
|---|---|---|
| 仅通过下一步按钮 | |
| 目标元素上的DOM事件 | |
| 基于计时器 | |
| URL变更时 | |
| 轮询条件满足时 | |
tsx
// 组合规则以提升灵活性
advance: [
{ type: 'event', event: 'click', on: 'target' },
{ type: 'delay', ms: 10000 }, // 10秒后自动推进作为备选
]React Integration
React集成
TourProvider Props
TourProvider 属性
tsx
<TourProvider
flows={[flow1, flow2]} // Flow definitions
storageNamespace="my-app" // localStorage key prefix
persistOnChange={true} // Auto-save state changes
backdropInteraction="block" // 'block' | 'passthrough'
lockBodyScroll={false} // Prevent page scroll
labels={{
// Customize UI text for internationalization
back: 'Zurück',
next: 'Weiter',
finish: 'Fertig',
skip: 'Tour überspringen',
// See Internationalization section for full list
}}
analytics={{
// Event handlers
onFlowStart: (p) => track('tour_start', p),
onStepEnter: (p) => track('step_view', p),
}}
/>tsx
<TourProvider
flows={[flow1, flow2]} // 引导流定义
storageNamespace="my-app" // localStorage键前缀
persistOnChange={true} // 自动保存状态变更
backdropInteraction="block" // 'block' | 'passthrough'
lockBodyScroll={false} // 禁止页面滚动
labels={{
// 自定义UI文本以支持国际化
back: 'Zurück',
next: 'Weiter',
finish: 'Fertig',
skip: 'Tour überspringen',
// 完整列表请参见国际化章节
}}
analytics={{
// 事件处理函数
onFlowStart: (p) => track('tour_start', p),
onStepEnter: (p) => track('step_view', p),
}}
/>useTour Hook
useTour 钩子
tsx
const {
activeFlowId, // Currently active flow ID or null
state, // FlowState: status, stepIndex, version
activeStep, // Current Step object
startFlow, // (flowId, options?) => start a flow
next, // () => advance to next step
back, // () => go to previous step
pause, // () => pause the flow
cancel, // (reason?) => cancel the flow
complete, // () => mark flow complete
advanceStep, // (stepId) => FlowState | null — advance only if on that step
} = useTour()tsx
const {
activeFlowId, // 当前激活的引导流ID或null
state, // FlowState:状态、步骤索引、版本
activeStep, // 当前步骤对象
startFlow, // (flowId, options?) => 启动引导流
next, // () => 推进到下一步
back, // () => 返回上一步
pause, // () => 暂停引导流
cancel, // (reason?) => 取消引导流
complete, // () => 标记引导流完成
advanceStep, // (stepId) => FlowState | null — 仅当当前在指定步骤时推进
} = useTour()Conditional Advance with advanceStep
使用advanceStep实现条件推进
Use when you want to advance the tour only if the user is currently on a specific step. This is useful for components that trigger tour progression as a side effect.
advanceStep(stepId)tsx
const { advanceStep } = useTour()
// In a logo upload component:
const handleLogoUpload = async (file: File) => {
await uploadLogo(file)
advanceStep('change-logo') // Only advances if tour is on 'change-logo' step
}Behavior:
- If currently on the specified step → advances to next (or completes if last step)
- If on a different step → silent no-op (returns current state)
- If stepId doesn't exist → silent no-op (not an error)
- If no active flow → returns (safe to call without checking flow state)
null
当你希望仅在用户当前处于特定步骤时推进引导流程时,使用。这在组件将引导推进作为副作用触发时非常有用。
advanceStep(stepId)tsx
const { advanceStep } = useTour()
// 在Logo上传组件中:
const handleLogoUpload = async (file: File) => {
await uploadLogo(file)
advanceStep('change-logo') // 仅当引导流程处于'change-logo'步骤时才推进
}行为:
- 如果当前处于指定步骤 → 推进到下一步(如果是最后一步则标记完成)
- 如果处于其他步骤 → 静默无操作(返回当前状态)
- 如果stepId不存在 → 静默无操作(不会报错)
- 如果没有激活的引导流 → 返回(无需检查流状态即可安全调用)
null
TourHUD Configuration
TourHUD 配置
tsx
<TourHUD
overlay={{
padding: 12, // Padding around highlight
radius: 12, // Border radius of cutout
showRing: true, // Glow effect around target
blurAmount: 6, // Backdrop blur (px)
}}
popover={{
maxWidth: 360,
offset: 32, // Distance from target
}}
controls={{
showSkip: true,
skipMode: 'hold', // 'click' | 'hold' (hold-to-confirm)
}}
progress={{
show: true,
variant: 'dots', // 'dots' | 'bar' | 'fraction'
}}
mobile={{
enabled: true, // Enable mobile drawer (default: true)
breakpoint: 640, // Width threshold for mobile (default: 640)
defaultSnapPoint: 'expanded', // Initial state (default: 'expanded')
snapPoints: ['minimized', 'expanded'], // Available states
allowMinimize: true, // Allow swipe down to minimize
}}
/>tsx
<TourHUD
overlay={{
padding: 12, // 高亮区域周围的内边距
radius: 12, // 镂空区域的圆角
showRing: true, // 目标周围的发光效果
blurAmount: 6, // 背景模糊程度(像素)
}}
popover={{
maxWidth: 360,
offset: 32, // 与目标的距离
}}
controls={{
showSkip: true,
skipMode: 'hold', // 'click' | 'hold'(长按确认)
}}
progress={{
show: true,
variant: 'dots', // 'dots' | 'bar' | 'fraction'
}}
mobile={{
enabled: true, // 启用移动端抽屉(默认:true)
breakpoint: 640, // 移动端宽度阈值(默认:640)
defaultSnapPoint: 'expanded', // 初始状态(默认:'expanded')
snapPoints: ['minimized', 'expanded'], // 可用状态
allowMinimize: true, // 允许向下滑动最小化
}}
/>Mobile Drawer
移动端抽屉
On viewports ≤640px, automatically renders a bottom sheet drawer instead of a floating popover. Users can swipe to minimize (see highlighted target) or expand (read content).
TourHUDSnap Points:
- (~100px) - Shows step indicator + nav buttons only
minimized - (~40% of expanded) - Optional middle state for summaries
peek - (auto) - Sized to content, capped at
expandedmaxHeightRatio
Gestures:
- Swipe down → minimize
- Swipe up → expand
- Tap handle → toggle between states
Behavior:
- Auto-sizes to content - Drawer height matches content + chrome (handle, header, controls)
- Capped at max - Won't exceed of viewport (default 85%)
maxHeightRatio - No flicker - Starts small, animates up once content is measured
- Resets to on step transitions
expanded - Content crossfades between steps
- Safe area insets for notched phones
- announcement when minimized
aria-live
Constrained Scroll Lock:
When body scroll lock is enabled and the highlighted target exceeds viewport height, constrained scroll lock allows scrolling within target bounds only:
- Target fits in viewport → normal scroll lock ()
overflow: hidden - Target exceeds viewport → scroll constrained to target bounds (user can see entire element)
tsx
// Auto-size with custom max height
<TourHUD
mobile={{
maxHeightRatio: 0.7, // Cap at 70% viewport
}}
/>
// Enable three-state drawer with peek
<TourHUD
mobile={{
snapPoints: ['minimized', 'peek', 'expanded'],
defaultSnapPoint: 'expanded',
}}
/>在视口宽度≤640px时,会自动渲染底部抽屉而非浮动弹出框。用户可以滑动来最小化(查看高亮目标)或展开(阅读内容)。
TourHUD停靠点:
- (约100px)- 仅显示步骤指示器和导航按钮
minimized - (约为展开状态的40%)- 可选的中间状态,用于展示摘要
peek - (自动)- 根据内容自动调整大小,上限为
expandedmaxHeightRatio
手势操作:
- 向下滑动 → 最小化
- 向上滑动 → 展开
- 点击手柄 → 在状态间切换
行为:
- 自动适配内容大小 - 抽屉高度匹配内容和控件(手柄、标题、导航)
- 最大高度限制 - 不会超过视口的(默认85%)
maxHeightRatio - 无闪烁切换 - 初始尺寸较小,测量内容后动画展开
- 步骤切换时重置为状态
expanded - 步骤内容之间淡入淡出切换
- 为刘海屏手机适配安全区域
- 最小化时通过播报状态
aria-live
约束滚动锁定:
当启用页面滚动锁定且高亮目标超出视口高度时,约束滚动锁定仅允许在目标元素范围内滚动:
- 目标在视口内 → 正常滚动锁定()
overflow: hidden - 目标超出视口 → 滚动被限制在目标元素范围内(用户可以看到完整元素)
tsx
// 自动适配大小并设置自定义最大高度
<TourHUD
mobile={{
maxHeightRatio: 0.7, // 上限为视口的70%
}}
/>
// 启用包含peek状态的三态抽屉
<TourHUD
mobile={{
snapPoints: ['minimized', 'peek', 'expanded'],
defaultSnapPoint: 'expanded',
}}
/>Common Mistakes
常见错误
-
Missingattributes - Tour cannot find elements
data-tour-targettsx// Bad: fragile to styling changes target: { selector: '.btn-primary' } // Good: semantic and stable target: { selector: '[data-tour-target="submit-btn"]' } -
Nofor async content - Step shows before content ready
waitFortsx// Add waitFor when targeting dynamically loaded elements waitFor: { selector: '[data-tour-target="api-result"]', timeout: 8000 } -
Ignoring sticky headers - Target scrolls behind fixed navigationtsx
targetBehavior: { scrollMargin: { top: 80 }, // Height of sticky header scrollMode: 'start', scrollDurationMs: 350, // Keep scroll timing aligned with HUD motion } -
Wrong version format - Use object, not numbertsx
// Bad version: 1 // Good version: { major: 1, minor: 0 } -
Forgettinghooks - UI state not restored after reload
onResumetsx// Bad: UI broken after page reload onEnter: () => ensureMenuOpen(), // Good: Both hooks restore UI state onEnter: () => ensureMenuOpen(), onResume: () => ensureMenuOpen(), onExit: () => ensureMenuClosed(),
-
缺失属性 - 引导流程无法找到元素
data-tour-targettsx// 错误:易受样式变更影响 target: { selector: '.btn-primary' } // 正确:语义化且稳定 target: { selector: '[data-tour-target="submit-btn"]' } -
未为异步内容设置- 步骤在内容准备好之前就显示
waitFortsx// 当目标是动态加载的元素时,添加waitFor waitFor: { selector: '[data-tour-target="api-result"]', timeout: 8000 } -
忽略粘性头部 - 目标元素滚动到固定导航栏后方tsx
targetBehavior: { scrollMargin: { top: 80 }, // 粘性头部的高度 scrollMode: 'start', scrollDurationMs: 350, // 保持滚动动画与HUD动效同步 } -
版本格式错误 - 使用对象而非数字tsx
// 错误 version: 1 // 正确 version: { major: 1, minor: 0 } -
忘记添加钩子 - 页面重新加载后UI状态未恢复
onResumetsx// 错误:页面重新加载后UI异常 onEnter: () => ensureMenuOpen(), // 正确:两个钩子都能恢复UI状态 onEnter: () => ensureMenuOpen(), onResume: () => ensureMenuOpen(), onExit: () => ensureMenuClosed(),
Scroll Synchronization for Long Jumps
长距离滚动同步
When consecutive steps are far apart on the page, set a fixed on the step:
scrollDurationMstsx
{
id: 'architecture',
target: { selector: '[data-tour-target="architecture"]' },
targetBehavior: {
scrollMode: 'center',
scrollDurationMs: 350,
},
}Guidelines:
- Use for most landing pages. Start with
250-450ms.350ms - Keep page-level CSS smooth scroll if you want; when is set, Flowsterix temporarily bypasses global CSS smooth scrolling so timing stays deterministic.
scrollDurationMs - During long jumps, overlay highlight and popover stay anchored to the previous on-screen position until the next target enters the viewport, then transition to the new target.
- Use for minimal movement,
scrollMode: 'preserve'for guided storytelling, orcenterwhen sticky headers need strict top alignment.start
当连续步骤在页面上相距较远时,为步骤设置固定的:
scrollDurationMstsx
{
id: 'architecture',
target: { selector: '[data-tour-target="architecture"]' },
targetBehavior: {
scrollMode: 'center',
scrollDurationMs: 350,
},
}指导原则:
- 大多数落地页使用,推荐从
250-450ms开始。350ms - 如果需要可以保留页面级CSS平滑滚动;当设置了时,Flowsterix会临时绕过全局CSS平滑滚动,以保证动画时间的确定性。
scrollDurationMs - 在长距离滚动过程中,遮罩高亮和弹出框会锚定在之前的屏幕位置,直到下一个目标进入视口,然后过渡到新目标。
- 使用实现最小移动,
scrollMode: 'preserve'用于引导式叙事,center用于粘性头部需要严格顶部对齐的场景。start
Shadcn Components
Shadcn组件
The shadcn registry provides preconfigured, polished components. Always prefer these over custom implementations.
Important: The tour components require shadcn CSS variables (,--popover,--border, etc.). If you're not using shadcn/ui, see CSS Setup for the required variables.--destructive
Shadcn注册表提供了预配置的精致组件。始终优先使用这些组件而非自定义实现。
重要提示:引导组件依赖shadcn CSS变量(、--popover、--border等)。如果未使用shadcn/ui,请参见CSS设置获取所需变量。--destructive
Available Components
可用组件
| Component | Install Command | Usage |
|---|---|---|
| | Full HUD with overlay & popover |
| | Step layout primitives |
| | Bottom sheet for mobile |
| | Swipe handle for drawer |
| 组件 | 安装命令 | 用途 |
|---|---|---|
| | 包含遮罩和弹出框的完整HUD |
| | 步骤布局基础组件 |
| | 移动端底部抽屉 |
| | 抽屉的滑动手柄 |
Step Content Primitives
步骤内容基础组件
Use these components for consistent step styling:
tsx
import {
StepContent,
StepTitle,
StepText,
StepHint,
} from '@/components/step-content'
content: (
<StepContent>
<StepTitle>Feature Discovery</StepTitle>
<StepText>
This is the main explanation text with muted styling.
</StepText>
<StepHint>Click the button to continue.</StepHint>
</StepContent>
)- - Grid container with proper spacing
StepContent - - Semibold heading (supports
StepTitlefor welcome screens)size="lg" - - Muted paragraph text
StepText - - Italic hint text for user instructions
StepHint
使用这些组件实现一致的步骤样式:
tsx
import {
StepContent,
StepTitle,
StepText,
StepHint,
} from '@/components/step-content'
content: (
<StepContent>
<StepTitle>功能探索</StepTitle>
<StepText>
这是带有弱化样式的主要说明文本。
</StepText>
<StepHint>点击按钮继续。</StepHint>
</StepContent>
)- - 带合适间距的网格容器
StepContent - - 半粗体标题(支持
StepTitle用于欢迎页面)size="lg" - - 弱化样式的段落文本
StepText - - 斜体提示文本,用于用户操作指引
StepHint
Radix Dialog Integration
Radix Dialog集成
Use for declarative dialog control during tours.
useRadixTourDialog使用在引导流程中声明式控制对话框。
useRadixTourDialogSetup
配置步骤
tsx
import { createFlow } from '@flowsterix/core'
import { useRadixTourDialog } from '@flowsterix/react'
import * as Dialog from '@radix-ui/react-dialog'
// 1. Configure dialogs in flow definition
const flow = createFlow({
id: 'onboarding',
version: { major: 1, minor: 0 },
dialogs: {
settings: {
autoOpen: true, // Open when entering dialog steps
autoClose: 'differentDialog', // Close when moving to non-dialog step
onDismissGoToStepId: 'settings-trigger', // Where to go if user closes dialog
},
},
steps: [
{ id: 'settings-trigger', target: '#settings-btn', content: 'Click here' },
{ id: 'settings-tab1', dialogId: 'settings', target: '#tab1', content: 'First tab' },
{ id: 'settings-tab2', dialogId: 'settings', target: '#tab2', content: 'Second tab' },
// Dialog stays open for consecutive steps with same dialogId
{ id: 'done', target: 'screen', content: 'All done' },
// Dialog auto-closes when entering 'done' (no dialogId)
],
})
// 2. Use hook in your dialog component
function SettingsDialog({ children }) {
const { dialogProps, contentProps } = useRadixTourDialog({ dialogId: 'settings' })
return (
<Dialog.Root {...dialogProps}>
<Dialog.Trigger data-tour-target="settings-trigger">Settings</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content {...contentProps} data-tour-target="settings-dialog">
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}tsx
import { createFlow } from '@flowsterix/core'
import { useRadixTourDialog } from '@flowsterix/react'
import * as Dialog from '@radix-ui/react-dialog'
// 1. 在引导流定义中配置对话框
const flow = createFlow({
id: 'onboarding',
version: { major: 1, minor: 0 },
dialogs: {
settings: {
autoOpen: true, // 进入对话框步骤时自动打开
autoClose: 'differentDialog', // 切换到非对话框步骤时自动关闭
onDismissGoToStepId: 'settings-trigger', // 用户关闭对话框后跳转的步骤ID
},
},
steps: [
{ id: 'settings-trigger', target: '#settings-btn', content: '点击此处' },
{ id: 'settings-tab1', dialogId: 'settings', target: '#tab1', content: '第一个标签页' },
{ id: 'settings-tab2', dialogId: 'settings', target: '#tab2', content: '第二个标签页' },
// 连续步骤使用相同dialogId时,对话框保持打开
{ id: 'done', target: 'screen', content: '全部完成' },
// 进入'done'步骤时(无dialogId),对话框自动关闭
],
})
// 2. 在对话框组件中使用钩子
function SettingsDialog({ children }) {
const { dialogProps, contentProps } = useRadixTourDialog({ dialogId: 'settings' })
return (
<Dialog.Root {...dialogProps}>
<Dialog.Trigger data-tour-target="settings-trigger">设置</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay />
<Dialog.Content {...contentProps} data-tour-target="settings-dialog">
{children}
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
)
}Dialog Configuration Options
对话框配置选项
tsx
dialogs: {
myDialog: {
// Auto-open behavior (default: true for both)
autoOpen: {
onEnter: true, // Open when entering a step with this dialogId
onResume: true, // Open when resuming to a step with this dialogId
},
// Or disable all auto-open:
autoOpen: false,
// Auto-close behavior (default: 'differentDialog')
autoClose: 'differentDialog', // Close when next step has different/no dialogId
autoClose: 'always', // Always close on step exit
autoClose: 'never', // Manual close only
// Required: where to navigate when user dismisses dialog
onDismissGoToStepId: 'some-step-id',
},
}tsx
dialogs: {
myDialog: {
// 自动打开行为(默认:两者均为true)
autoOpen: {
onEnter: true, // 进入带有此dialogId的步骤时打开
onResume: true, // 恢复到带有此dialogId的步骤时打开
},
// 或者禁用所有自动打开:
autoOpen: false,
// 自动关闭行为(默认:'differentDialog')
autoClose: 'differentDialog', // 下一步骤的dialogId不同或无时关闭
autoClose: 'always', // 离开步骤时始终关闭
autoClose: 'never', // 仅手动关闭
// 必填:用户关闭对话框后跳转的步骤ID
onDismissGoToStepId: 'some-step-id',
},
}Focus Management: useRadixDialogAdapter
焦点管理:useRadixDialogAdapter
For dialogs without tour integration that still need focus handling during tours:
tsx
import { useRadixDialogAdapter } from '@flowsterix/react'
function SimpleDialog({ children }) {
const { dialogProps, contentProps } = useRadixDialogAdapter({
disableEscapeClose: true,
})
return (
<Dialog.Root {...dialogProps}>
<Dialog.Content {...contentProps}>{children}</Dialog.Content>
</Dialog.Root>
)
}对于未集成引导流程但仍需在引导期间处理焦点的对话框:
tsx
import { useRadixDialogAdapter } from '@flowsterix/react'
function SimpleDialog({ children }) {
const { dialogProps, contentProps } = useRadixDialogAdapter({
disableEscapeClose: true,
})
return (
<Dialog.Root {...dialogProps}>
<Dialog.Content {...contentProps}>{children}</Dialog.Content>
</Dialog.Root>
)
}Lifecycle Hooks
生命周期钩子
Lifecycle hooks synchronize UI state with tour progression. Use them when steps target elements inside collapsible panels, modals, drawers, or other dynamic UI.
生命周期钩子用于同步UI状态与引导流程进度。当步骤目标位于可折叠面板、模态框、抽屉或其他动态UI内部时,请使用这些钩子。
When to Use Each Hook
各钩子的适用场景
| Hook | Fires When | Purpose |
|---|---|---|
| Step activates (fresh start) | Open UI, prepare state |
| Step restores from storage | Restore UI after page reload |
| Leaving step (next/back/skip) | Clean up, close UI |
| 钩子 | 触发时机 | 用途 |
|---|---|---|
| 步骤激活时(首次进入) | 打开UI、准备状态 |
| 从存储恢复步骤时 | 页面重新加载后恢复UI状态 |
| 离开步骤时(下一步/上一步/跳过) | 清理、关闭UI |
Common Patterns
常见模式
1. Opening/Closing Drawers & Menus
tsx
// Helper functions to toggle menu state
const ensureMenuOpen = () => {
const panel = document.querySelector('[data-tour-target="menu-panel"]')
if (!(panel instanceof HTMLElement)) return
const isClosed = panel.classList.contains('-translate-x-full')
if (isClosed) {
document.querySelector('[data-tour-target="menu-button"]')?.click()
}
}
const ensureMenuClosed = () => {
const panel = document.querySelector('[data-tour-target="menu-panel"]')
if (!(panel instanceof HTMLElement)) return
const isClosed = panel.classList.contains('-translate-x-full')
if (!isClosed) {
panel.querySelector('[aria-label="Close menu"]')?.click()
}
}2. Step Targeting Element Inside Drawer
tsx
{
id: 'menu-link',
target: { selector: '[data-tour-target="api-link"]' },
onEnter: () => ensureMenuOpen(), // Open drawer on fresh entry
onResume: () => ensureMenuOpen(), // Open drawer on page reload
onExit: () => ensureMenuClosed(), // Close drawer when leaving
advance: [{ type: 'route', to: '/api-demo' }],
content: (
<StepContent>
<StepTitle>API Demo</StepTitle>
<StepText>Click to explore the API features.</StepText>
</StepContent>
),
}3. Expanding Nested Accordions
tsx
const ensureAccordionExpanded = () => {
ensureMenuOpen() // Parent must be open first
const submenu = document.querySelector('[data-tour-target="submenu"]')
if (submenu) return // Already expanded
document.querySelector('[data-tour-target="accordion-toggle"]')?.click()
}
{
id: 'submenu-item',
target: { selector: '[data-tour-target="submenu"]' },
onResume: () => ensureAccordionExpanded(),
content: ...
}4. Closing UI When Moving Away
tsx
{
id: 'feature-grid',
target: { selector: '#feature-grid' },
onEnter: () => {
setTimeout(() => ensureMenuClosed(), 0) // Allow menu click to register first
},
onResume: () => ensureMenuClosed(),
content: ...
}1. 打开/关闭抽屉与菜单
tsx
// 切换菜单状态的辅助函数
const ensureMenuOpen = () => {
const panel = document.querySelector('[data-tour-target="menu-panel"]')
if (!(panel instanceof HTMLElement)) return
const isClosed = panel.classList.contains('-translate-x-full')
if (isClosed) {
document.querySelector('[data-tour-target="menu-button"]')?.click()
}
}
const ensureMenuClosed = () => {
const panel = document.querySelector('[data-tour-target="menu-panel"]')
if (!(panel instanceof HTMLElement)) return
const isClosed = panel.classList.contains('-translate-x-full')
if (!isClosed) {
panel.querySelector('[aria-label="Close menu"]')?.click()
}
}2. 目标位于抽屉内的步骤
tsx
{
id: 'menu-link',
target: { selector: '[data-tour-target="api-link"]' },
onEnter: () => ensureMenuOpen(), // 首次进入时打开抽屉
onResume: () => ensureMenuOpen(), // 页面重新加载后打开抽屉
onExit: () => ensureMenuClosed(), // 离开时关闭抽屉
advance: [{ type: 'route', to: '/api-demo' }],
content: (
<StepContent>
<StepTitle>API演示</StepTitle>
<StepText>点击此处探索API功能。</StepText>
</StepContent>
),
}3. 展开嵌套折叠面板
tsx
const ensureAccordionExpanded = () => {
ensureMenuOpen() // 首先确保父菜单已打开
const submenu = document.querySelector('[data-tour-target="submenu"]')
if (submenu) return // 已展开
document.querySelector('[data-tour-target="accordion-toggle"]')?.click()
}
{
id: 'submenu-item',
target: { selector: '[data-tour-target="submenu"]' },
onResume: () => ensureAccordionExpanded(),
content: ...
}4. 切换到其他步骤时关闭UI
tsx
{
id: 'feature-grid',
target: { selector: '#feature-grid' },
onEnter: () => {
setTimeout(() => ensureMenuClosed(), 0) // 等待菜单点击事件完成注册
},
onResume: () => ensureMenuClosed(),
content: ...
}Critical Rules
关键规则
- Always implement when you have
onResume- Users may reload the page mid-touronEnter - Check state before acting - Don't toggle already-open menus
- Use for sequential actions - Give previous clicks time to register
setTimeout - Keep hooks idempotent - Safe to call multiple times
- 当实现时,务必同时实现
onEnter- 用户可能在引导过程中重新加载页面onResume - 操作前检查状态 - 不要重复切换已打开的菜单
- 使用处理连续操作 - 给之前的点击事件留出注册时间
setTimeout - 保持钩子幂等性 - 多次调用也能保证安全
Step Placements
步骤位置选项
'auto' | 'top' | 'bottom' | 'left' | 'right' |
'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' |
'left-start' | 'left-end' | 'right-start' | 'right-end' |
'auto-start' | 'auto-end''auto' | 'top' | 'bottom' | 'left' | 'right' |
'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' |
'left-start' | 'left-end' | 'right-start' | 'right-end' |
'auto-start' | 'auto-end'Route Gating
路由限制
Steps can be constrained to specific routes using the property. The flow automatically pauses when the user navigates away and resumes when they return.
route可以通过属性将步骤限制在特定路由上。当用户导航到其他路由时,引导流会自动暂停,返回时自动恢复。
routeRoute Mismatch Behavior
路由不匹配时的行为
tsx
{
id: 'dashboard-feature',
target: { selector: '[data-tour-target="widget"]' },
route: '/dashboard', // Step only active on /dashboard
content: <p>This widget shows your stats</p>,
}Behavior when user navigates away from :
/dashboard- Flow pauses immediately (overlay disappears)
- User can browse other pages freely
- When user returns to , flow auto-resumes
/dashboard
tsx
{
id: 'dashboard-feature',
target: { selector: '[data-tour-target="widget"]' },
route: '/dashboard', // 仅在/dashboard路由上激活
content: <p>此小组件展示您的统计数据</p>,
}用户离开路由时的行为:
/dashboard- 引导流立即暂停(遮罩消失)
- 用户可以自由浏览其他页面
- 当用户返回时,引导流自动恢复
/dashboard
Missing Target Behavior (No Route Defined)
未定义路由时的目标缺失行为
When a step has no property and the target element is missing:
route- Grace period (400ms) - Allows async elements to mount
- If still missing → Flow pauses
- When user navigates to a different page → Flow resumes and re-checks
- If target found → Flow continues
- If still missing → Grace period → Pause again
This prevents showing broken UI when users accidentally navigate away.
当步骤未定义属性且目标元素缺失时:
route- 宽限期(400ms)- 允许异步元素完成挂载
- 如果仍缺失 → 引导流暂停
- 当用户导航到其他页面 → 引导流恢复并重新检查
- 如果找到目标 → 引导流继续
- 如果仍缺失 → 宽限期 → 再次暂停
这可以避免用户意外导航时显示损坏的UI。
Route Patterns
路由匹配模式
tsx
// Exact match
route: '/dashboard'
// Regex pattern
route: /^\/users\/\d+$/
// With path parameters (use regex)
route: /^\/products\/[^/]+$/tsx
// 精确匹配
route: '/dashboard'
// 正则表达式匹配
route: /^\/users\/\d+$/
// 带路径参数(使用正则)
route: /^\/products\/[^/]+$/Internationalization (i18n)
国际化(i18n)
All user-facing text can be customized via the prop on .
labelsTourProvider所有面向用户的文本都可以通过的属性自定义。
TourProviderlabelsAvailable Labels
可用标签
tsx
<TourProvider
flows={[...]}
labels={{
// Button labels
back: 'Back',
next: 'Next',
finish: 'Finish',
skip: 'Skip tour',
holdToConfirm: 'Hold to confirm',
// Aria labels for screen readers
ariaStepProgress: ({ current, total }) => `Step ${current} of ${total}`,
ariaTimeRemaining: ({ ms }) => `${Math.ceil(ms / 1000)} seconds remaining`,
ariaDelayProgress: 'Auto-advance progress',
// Visible formatters
formatTimeRemaining: ({ ms }) => `${Math.ceil(ms / 1000)}s remaining`,
// Target issue messages (shown when target element is problematic)
targetIssue: {
missingTitle: 'Target not visible',
missingBody: 'The target element is not currently visible...',
missingHint: 'Showing the last known position until the element returns.',
hiddenTitle: 'Target not visible',
hiddenBody: 'The target element is not currently visible...',
hiddenHint: 'Showing the last known position until the element returns.',
detachedTitle: 'Target left the page',
detachedBody: 'Navigate back to the screen that contains this element...',
},
}}
>tsx
<TourProvider
flows={[...]}
labels={{
// 按钮文本
back: '返回',
next: '下一步',
finish: '完成',
skip: '跳过引导',
holdToConfirm: '长按确认',
// 屏幕阅读器的Aria标签
ariaStepProgress: ({ current, total }) => `第${current}步,共${total}步`,
ariaTimeRemaining: ({ ms }) => `剩余${Math.ceil(ms / 1000)}秒`,
ariaDelayProgress: '自动推进进度',
// 可见的格式化文本
formatTimeRemaining: ({ ms }) => `剩余${Math.ceil(ms / 1000)}秒`,
// 目标元素异常提示信息(当目标元素出现问题时显示)
targetIssue: {
missingTitle: '目标不可见',
missingBody: '目标元素当前不可见...',
missingHint: '显示最后已知位置,直到元素重新出现。',
hiddenTitle: '目标不可见',
hiddenBody: '目标元素当前不可见...',
hiddenHint: '显示最后已知位置,直到元素重新出现。',
detachedTitle: '目标已离开页面',
detachedBody: '请导航回包含此元素的页面...',
},
}}
>German Example
德语示例
tsx
const germanLabels = {
back: 'Zurück',
next: 'Weiter',
finish: 'Fertig',
skip: 'Tour überspringen',
holdToConfirm: 'Gedrückt halten zum Bestätigen',
ariaStepProgress: ({ current, total }) => `Schritt ${current} von ${total}`,
targetIssue: {
missingTitle: 'Ziel nicht sichtbar',
missingBody: 'Das Zielelement ist derzeit nicht sichtbar.',
detachedTitle: 'Ziel hat die Seite verlassen',
detachedBody: 'Navigieren Sie zurück zur Seite mit diesem Element.',
// ... other labels
},
}
<TourProvider flows={[...]} labels={germanLabels}>tsx
const germanLabels = {
back: 'Zurück',
next: 'Weiter',
finish: 'Fertig',
skip: 'Tour überspringen',
holdToConfirm: 'Gedrückt halten zum Bestätigen',
ariaStepProgress: ({ current, total }) => `Schritt ${current} von ${total}`,
targetIssue: {
missingTitle: 'Ziel nicht sichtbar',
missingBody: 'Das Zielelement ist derzeit nicht sichtbar.',
detachedTitle: 'Ziel hat die Seite verlassen',
detachedBody: 'Navigieren Sie zurück zur Seite mit diesem Element.',
// ... 其他标签
},
}
<TourProvider flows={[...]} labels={germanLabels}>DevTools
开发者工具
The subpath provides development tools for building and debugging tours:
@flowsterix/react/devtools- Steps tab - Visual element picker to capture tour steps and export JSON for AI
- Flows tab - View and edit stored flow states for debugging
@flowsterix/react/devtools- 步骤标签页 - 可视化元素选取器,用于捕获引导步骤并导出JSON供AI使用
- 引导流标签页 - 查看和编辑存储的引导流状态以进行调试
Setup
配置步骤
tsx
import { DevToolsProvider } from '@flowsterix/react/devtools'
function App() {
return (
<TourProvider flows={[...]}>
<DevToolsProvider enabled={process.env.NODE_ENV === 'development'}>
<YourApp />
</DevToolsProvider>
</TourProvider>
)
}tsx
import { DevToolsProvider } from '@flowsterix/react/devtools'
function App() {
return (
<TourProvider flows={[...]}>
<DevToolsProvider enabled={process.env.NODE_ENV === 'development'}>
<YourApp />
</DevToolsProvider>
</TourProvider>
)
}Steps Tab (Element Grabber)
步骤标签页(元素选取器)
- Press to toggle grab mode
Ctrl+Shift+G - Click elements to capture as tour steps
- Drag to reorder steps in the panel
- Click "Copy" to export JSON for AI
- 按切换选取模式
Ctrl+Shift+G - 点击元素以捕获为引导步骤
- 在面板中拖动以重新排序步骤
- 点击“复制”导出JSON供AI使用
Export Format
导出格式
json
{
"version": "1.0",
"steps": [
{
"order": 0,
"element": "<button class=\"btn-primary\">Get Started</button>",
"componentTree": ["button", "Button", "Header", "App"]
}
]
}json
{
"version": "1.0",
"steps": [
{
"order": 0,
"element": "<button class=\"btn-primary\">Get Started</button>",
"componentTree": ["button", "Button", "Header", "App"]
}
]
}AI Workflow
AI工作流
- Capture elements with devtools
- Copy the JSON export
- Paste into AI with prompt: "Create a Flowsterix tour flow for these elements"
- AI generates flow definition with proper selectors
data-tour-target
- 使用开发者工具捕获元素
- 复制JSON导出内容
- 将其粘贴到AI中,并提示:“为这些元素创建一个Flowsterix引导流”
- AI生成带有正确选择器的引导流定义
data-tour-target
Keyboard Shortcuts
键盘快捷键
| Shortcut | Action |
|---|---|
| Toggle grab mode |
| Cancel grab mode |
| Collapse/expand panel |
| 快捷键 | 操作 |
|---|---|
| 切换选取模式 |
| 取消选取模式 |
| 折叠/展开面板 |
Flows Tab
引导流标签页
The Flows tab shows all registered flows and their stored state. Use it to:
- View flow status - See which flows are idle, running, paused, completed, or cancelled
- Inspect state - Check current step index, version, and step ID
- Edit flow state - Modify stored JSON directly (useful for debugging)
- Delete flow state - Clear stored state to reset a flow (cancels if running)
Features:
- Live updates when active flow state changes
- Shows "Active" badge for currently running flow
- Confirmation required before deleting
Use cases:
- Reset a flow to test from beginning
- Debug unexpected flow behavior by inspecting stored state
- Manually advance a stuck flow by editing
stepIndex - Clear completed flows to re-trigger
autoStart
引导流标签页显示所有已注册的引导流及其存储的状态。可用于:
- 查看引导流状态 - 查看哪些引导流处于空闲、运行、暂停、完成或取消状态
- 检查状态详情 - 查看当前步骤索引、版本和步骤ID
- 编辑引导流状态 - 直接修改存储的JSON(调试时非常有用)
- 删除引导流状态 - 清除存储的状态以重置引导流(如果正在运行则取消)
功能特性:
- 引导流状态变更时实时更新
- 为当前运行的引导流显示“Active”徽章
- 删除前需要确认
使用场景:
- 重置引导流以从头开始测试
- 通过检查存储的状态调试意外的引导流行为
- 通过编辑手动推进卡住的引导流
stepIndex - 清除已完成的引导流以重新触发
autoStart
Additional Resources
额外资源
- CSS Setup - Required shadcn CSS variables
- Flow Patterns - Targeting, advance rules, waitFor
- React Integration - Hooks, events, step content
- Router Adapters - TanStack, React Router, Next.js
- Advanced Patterns - Versions, storage, migrations
- Mobile Support - Mobile drawer, snap points, gestures
- CSS设置 - 所需的shadcn CSS变量
- 引导流模式 - 目标定位、推进规则、waitFor
- React集成 - 钩子、事件、步骤内容
- 路由适配器 - TanStack、React Router、Next.js
- 高级模式 - 版本、存储、迁移
- 移动端支持 - 移动端抽屉、停靠点、手势
Examples
示例
- Basic Flow - Simple 3-step onboarding
- Async Content - waitFor patterns
- Lifecycle Hooks - UI synchronization
- Router Sync - All 4 router adapters
- 基础引导流 - 简单的3步入门引导
- 异步内容 - waitFor模式
- 生命周期钩子 - UI同步
- 路由同步 - 所有4种路由适配器