flowsterix-best-practices

Flowsterix Best Practices

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "flowsterix-best-practices" with this command: npx skills add kvngrf/flowsterix/kvngrf-flowsterix-flowsterix-best-practices

Flowsterix Best Practices

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.

Quick Start

Installation

Core packages

npm install @flowsterix/core @flowsterix/react motion

Recommended: Add preconfigured shadcn components

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.

Minimal Example

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> ) }

Core Concepts

FlowDefinition

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 })

Step Anatomy

{ 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 }

Step Targets

// 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 data-tour-target attributes instead of CSS classes for stability.

Advance Rules

Rules define when a step automatically progresses. First matching rule wins.

Type Usage Example

manual

Next button only { type: 'manual' }

event

DOM event on target { type: 'event', event: 'click', on: 'target' }

delay

Timer-based { type: 'delay', ms: 3000 }

route

URL change { type: 'route', to: '/dashboard' }

predicate

Polling condition { type: 'predicate', check: (ctx) => isReady() }

// Combine rules for flexibility advance: [ { type: 'event', event: 'click', on: 'target' }, { type: 'delay', ms: 10000 }, // Fallback after 10s ]

React Integration

TourProvider Props

<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), }} />

useTour Hook

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()

Conditional Advance with advanceStep

Use advanceStep(stepId) 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.

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 null (safe to call without checking flow state)

TourHUD Configuration

<TourHUD overlay={{ padding: 12, // Padding around highlight radius: 12, // Border radius of cutout showRing: true, // Glow effect around target blurAmount: 6, // Controls unified faux-glow intensity }} 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 }} />

Overlay rendering model: Flowsterix now uses a unified SVG-based overlay path across desktop and mobile. Avoid browser-specific mask toggles and optimize visuals through padding , radius , showRing , and blurAmount .

Mobile Drawer

On viewports ≤640px, TourHUD automatically renders a bottom sheet drawer instead of a floating popover. Users can swipe to minimize (see highlighted target) or expand (read content).

Snap Points:

  • minimized (~100px) - Shows step indicator + nav buttons only

  • peek (~40% of expanded) - Optional middle state for summaries

  • expanded (auto) - Sized to content, capped at maxHeightRatio

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 maxHeightRatio of viewport (default 85%)

  • No flicker - Starts small, animates up once content is measured

  • Resets to expanded on step transitions

  • Content crossfades between steps

  • Safe area insets for notched phones

  • aria-live announcement when minimized

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)

// 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', }} />

Common Mistakes

Missing data-tour-target attributes - Tour cannot find elements

// Bad: fragile to styling changes target: { selector: '.btn-primary' }

// Good: semantic and stable target: { selector: '[data-tour-target="submit-btn"]' }

No waitFor for async content - Step shows before content ready

// Add waitFor when targeting dynamically loaded elements waitFor: { selector: '[data-tour-target="api-result"]', timeout: 8000 }

Ignoring sticky headers - Target scrolls behind fixed navigation

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 number

// Bad version: 1

// Good version: { major: 1, minor: 0 }

Forgetting onResume hooks - UI state not restored after reload

// Bad: UI broken after page reload onEnter: () => ensureMenuOpen(),

// Good: Both hooks restore UI state onEnter: () => ensureMenuOpen(), onResume: () => ensureMenuOpen(), onExit: () => ensureMenuClosed(),

Scroll Synchronization for Long Jumps

When consecutive steps are far apart on the page, set a fixed scrollDurationMs on the step:

{ id: 'architecture', target: { selector: '[data-tour-target="architecture"]' }, targetBehavior: { scrollMode: 'center', scrollDurationMs: 350, }, }

Guidelines:

  • Use 250-450ms for most landing pages. Start with 350ms .

  • Keep page-level CSS smooth scroll if you want; when scrollDurationMs is set, Flowsterix temporarily bypasses global CSS smooth scrolling so timing stays deterministic.

  • 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 scrollMode: 'preserve' for minimal movement, center for guided storytelling, or start when sticky headers need strict top alignment.

Shadcn Components

The shadcn registry provides preconfigured, polished components. Always prefer these over custom implementations.

Important: The tour components require shadcn CSS variables (--popover , --border , --destructive , etc.). If you're not using shadcn/ui, see CSS Setup for the required variables.

Available Components

Component Install Command Usage

tour-hud

npx shadcn@latest add https://flowsterix.com/r/tour-hud.json

Full HUD with overlay & popover

step-content

npx shadcn@latest add https://flowsterix.com/r/step-content.json

Step layout primitives

mobile-drawer

npx shadcn@latest add https://flowsterix.com/r/mobile-drawer.json

Bottom sheet for mobile

mobile-drawer-handle

npx shadcn@latest add https://flowsterix.com/r/mobile-drawer-handle.json

Swipe handle for drawer

Step Content Primitives

Use these components for consistent step styling:

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> )

  • StepContent

  • Grid container with proper spacing

  • StepTitle

  • Semibold heading (supports size="lg" for welcome screens)

  • StepText

  • Muted paragraph text

  • StepHint

  • Italic hint text for user instructions

Radix Dialog Integration

Use useRadixTourDialog for declarative dialog control during tours.

Setup

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> ) }

Dialog Configuration Options

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',

}, }

Focus Management: useRadixDialogAdapter

For dialogs without tour integration that still need focus handling during tours:

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.

When to Use Each Hook

Hook Fires When Purpose

onEnter

Step activates (fresh start) Open UI, prepare state

onResume

Step restores from storage Restore UI after page reload

onExit

Leaving step (next/back/skip) Clean up, close UI

Common Patterns

  1. Opening/Closing Drawers & Menus

// 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() } }

  1. Step Targeting Element Inside Drawer

{ 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> ), }

  1. Expanding Nested Accordions

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: ... }

  1. Closing UI When Moving Away

{ id: 'feature-grid', target: { selector: '#feature-grid' }, onEnter: () => { setTimeout(() => ensureMenuClosed(), 0) // Allow menu click to register first }, onResume: () => ensureMenuClosed(), content: ... }

Critical Rules

  • Always implement onResume when you have onEnter

  • Users may reload the page mid-tour

  • Check state before acting - Don't toggle already-open menus

  • Use setTimeout for sequential actions - Give previous clicks time to register

  • Keep hooks idempotent - Safe to call multiple times

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'

Route Gating

Steps can be constrained to specific routes using the route property. The flow automatically pauses when the user navigates away and resumes when they return.

Route Mismatch Behavior

{ 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 /dashboard , flow auto-resumes

Missing Target Behavior (No Route Defined)

When a step has no route property and the target element is missing:

  • 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 Patterns

// Exact match route: '/dashboard'

// Regex pattern route: /^/users/\d+$/

// With path parameters (use regex) route: /^/products/[^/]+$/

Internationalization (i18n)

All user-facing text can be customized via the labels prop on TourProvider .

Available Labels

<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...',
},

}}

German Example

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}>

DevTools

The @flowsterix/react/devtools subpath provides development tools for building and debugging tours:

  • Steps tab - Visual element picker to capture tour steps and export JSON for AI

  • Flows tab - View and edit stored flow states for debugging

Setup

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 Ctrl+Shift+G to toggle grab mode

  • Click elements to capture as tour steps

  • Drag to reorder steps in the panel

  • Click "Copy" to export JSON for AI

Export Format

{ "version": "1.0", "steps": [ { "order": 0, "element": "<button class="btn-primary">Get Started</button>", "componentTree": ["button", "Button", "Header", "App"] } ] }

AI Workflow

  • 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 data-tour-target selectors

Keyboard Shortcuts

Shortcut Action

Ctrl+Shift+G

Toggle grab mode

Esc

Cancel grab mode

Ctrl+Shift+M

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

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

Examples

  • Basic Flow - Simple 3-step onboarding

  • Async Content - waitFor patterns

  • Lifecycle Hooks - UI synchronization

  • Router Sync - All 4 router adapters

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review
General

studio-sdk

No summary provided by upstream source.

Repository SourceNeeds Review
General

PanchangaAPI — Vedic Astrology

Vedic astrology (Jyotish) REST API powered by Swiss Ephemeris. 24 endpoints: Panchanga, Kundali (300+ Yogas, Ashtakavarga, Doshas), KP system (249 sub-lords)...

Registry SourceRecently Updated
General

OPC Invoice Manager

Accounts Receivable light system for solo entrepreneurs. Manages the full billing lifecycle: invoice generation, collections follow-up, payment reconciliatio...

Registry SourceRecently Updated