@korsolutions/guidon
v1.1.2
Published
A cross-platform walkthrough/onboarding component library for React Native with web support. Features spotlight effects, customizable tooltips, and flexible persistence options.
Readme
Guidon
A lightweight, cross-platform walkthrough/onboarding library for React Native with web support. Features spotlight effects, draggable tooltips, and flexible persistence options.
Features
- Zero state management dependencies - Built with React's
useSyncExternalStore - New Architecture ready - Full support for React Native's Fabric renderer
- Cross-platform - iOS, Android, and Web (React Native Web)
- Draggable tooltips - Users can reposition tooltips by dragging
- Multi-screen tours - Guides pause when targets aren't mounted, resume when user navigates
- Floating steps - Create announcement/welcome screens without targeting elements
- Flexible persistence - Save progress to localStorage, AsyncStorage, or your API
- Customizable - Full theming, custom tooltip renderers, and per-step callbacks
Installation
yarn add @korsolutions/guidon
# or
npm install @korsolutions/guidonPeer Dependencies
Make sure you have these dependencies installed:
yarn add react react-native react-native-reanimated react-native-safe-area-context react-native-svgQuick Start
1. Configure Tours (at app root)
import { Guidon, GuidonProvider, GuidonToursConfig } from '@korsolutions/guidon';
// Configure all tours ONCE outside React (stable reference)
const config: GuidonToursConfig = {
// Global theme applies to all tours
theme: {
primaryColor: '#007AFF',
backdropOpacity: 0.8,
},
animationDuration: 300,
// Define your tours
tours: {
explore: {
id: 'explore',
steps: [
{ id: 'search', targetId: 'search-btn', title: 'Search', description: 'Find what you need' },
{ id: 'filter', targetId: 'filter-btn', title: 'Filter', description: 'Narrow results' },
],
onComplete: () => console.log('Explore tour done'),
},
profile: {
id: 'profile',
steps: [
{ id: 'edit', targetId: 'edit-btn', title: 'Edit Profile', description: 'Update your info' },
],
// Override theme just for this tour
theme: { primaryColor: '#FF6B6B' },
},
},
} satisfies GuidonToursConfig;
Guidon.configureTours(config);
// In your app root
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<GuidonProvider>
<Navigation />
</GuidonProvider>
</QueryClientProvider>
);
}2. Start Tours from Screens
import { useEffect } from 'react';
import { Guidon, useGuidon } from '@korsolutions/guidon';
function ExploreScreen() {
const { register } = useGuidon();
useEffect(() => {
Guidon.start('explore'); // Start this screen's tour
return () => Guidon.stop(); // Stop when leaving screen
}, []);
return (
<View>
<Button ref={register('search-btn')} title="Search" />
<Button ref={register('filter-btn')} title="Filter" />
</View>
);
}Alternative: GuidonTarget Component
import { GuidonTarget } from '@korsolutions/guidon';
function ExploreScreen() {
return (
<View>
<GuidonTarget targetId="search-btn">
<Button title="Search" />
</GuidonTarget>
<GuidonTarget targetId="filter-btn">
<Button title="Filter" />
</GuidonTarget>
</View>
);
}Floating Steps (No Target)
Create steps without a target element for welcome screens or announcements:
const config = {
tours: {
welcome: {
id: 'welcome',
steps: [
{
id: 'intro',
// No targetId - this is a floating step!
title: 'Welcome to Our App!',
description: 'Let us show you around.',
floatingPosition: 'center', // 'center' | 'top' | 'bottom' | 'top-left' | etc.
},
{
id: 'feature-1',
targetId: 'some-button',
title: 'First Feature',
description: 'This highlights an element.',
},
],
},
},
} satisfies GuidonToursConfig;Draggable Tooltips
Enable draggable tooltips so users can reposition them if they're blocking content:
const config = {
theme: {
tooltipDraggable: true,
dragHintText: 'Drag to reposition', // Optional custom hint
},
tours: {
// ...
},
} satisfies GuidonToursConfig;When enabled:
- A hint ("Drag to move") appears on the first step
- Users can drag the tooltip to any position on screen
- Position resets when advancing to the next step
- Works on both mobile (touch) and web (mouse)
Multi-Screen Tours
Tours can span multiple screens. When a step targets an element that isn't mounted yet, the guide shows a waiting state until the user navigates to the correct screen:
const config = {
// Auto-skip after 10 seconds if target doesn't appear
defaultWaitTimeout: 10000,
tours: {
onboarding: {
id: 'onboarding',
steps: [
{ id: 'home', targetId: 'home-button', title: 'Welcome', description: 'Start here' },
{
id: 'settings',
targetId: 'settings-toggle', // On Settings screen
title: 'Settings',
description: 'Configure your preferences',
waitingMessage: 'Navigate to Settings to continue...', // Custom waiting text
},
],
},
},
} satisfies GuidonToursConfig;The guide will:
- Show a loading indicator when the target element isn't found
- Display the
waitingMessage(or default "Navigate to continue...") - Automatically resume when the user navigates and the target mounts
- Optionally auto-skip after
defaultWaitTimeoutmilliseconds
Persistence
The library supports flexible persistence through adapters. You can save guidon progress to local storage, AsyncStorage, or your backend API.
Using Built-in Adapters
import {
GuidonProvider,
createLocalStorageAdapter,
createAsyncStorageAdapter,
} from '@korsolutions/guidon';
import AsyncStorage from '@react-native-async-storage/async-storage';
// For web (localStorage)
const webAdapter = createLocalStorageAdapter();
// For React Native (AsyncStorage)
const nativeAdapter = createAsyncStorageAdapter(AsyncStorage);
function App() {
return (
<GuidonProvider persistenceAdapter={nativeAdapter}>
<YourApp />
</GuidonProvider>
);
}Creating a Custom API Adapter
import { createApiAdapter } from '@korsolutions/guidon';
const apiAdapter = createApiAdapter({
loadProgress: async (guidonId) => {
const response = await fetch(`/api/guidon/${guidonId}`);
if (!response.ok) return null;
return response.json();
},
saveProgress: async (progress) => {
await fetch(`/api/guidon/${progress.guidonId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(progress),
});
},
clearProgress: async (guidonId) => {
await fetch(`/api/guidon/${guidonId}`, {
method: 'DELETE',
});
},
});Combining Multiple Adapters
import { createCompositeAdapter, createLocalStorageAdapter } from '@korsolutions/guidon';
// Save to both local storage and API
const compositeAdapter = createCompositeAdapter([
createLocalStorageAdapter(),
apiAdapter,
]);API Reference
Guidon API (Imperative)
Use outside React components or for programmatic control:
import { Guidon } from '@korsolutions/guidon';
// Configuration
Guidon.configureTours(config); // Configure all tours
// Control
Guidon.start('explore'); // Start a specific tour by ID
Guidon.stop(); // Stop current tour without completing
Guidon.next(); // Go to next step
Guidon.previous(); // Go to previous step
Guidon.goToStep(2); // Jump to specific step
Guidon.skip(); // Skip the tour
Guidon.complete(); // Complete the tour
// State queries
Guidon.isActive(); // Check if a tour is running
Guidon.isCompleted(); // Check if tour completed
Guidon.getActiveTourId(); // Get current tour ID
Guidon.getTours(); // Get all configured tours
Guidon.getCurrentStepIndex(); // Get current step index
Guidon.getCurrentStep(); // Get current step object
Guidon.getSteps(); // Get all steps of active tour
// Reset
Guidon.reset(); // Reset to initial stateGuidonProvider Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| autoStart | string | - | Tour ID to auto-start when mounted |
| shouldStart | () => boolean \| Promise<boolean> | - | Custom condition for starting |
| persistenceAdapter | GuidonPersistenceAdapter | - | Adapter for saving progress |
| portalComponent | React.ComponentType | - | Custom portal for overlay rendering |
| renderTooltip | (props) => ReactNode | - | Custom tooltip renderer |
| tooltipLabels | object | - | Customize button labels |
| onBackdropPress | () => void | - | Called when backdrop is pressed |
GuidonToursConfig
import { GuidonToursConfig } from '@korsolutions/guidon';
const config = {
// Global settings apply to all tours
theme: {
primaryColor: '#007AFF',
backdropOpacity: 0.8,
},
animationDuration: 300,
// Define tours
tours: {
explore: {
id: 'explore',
steps: [{ id: 's1', title: 'Welcome', description: '...' }],
onComplete: () => console.log('Done'),
},
profile: {
id: 'profile',
steps: [...],
theme: { primaryColor: '#FF0000' }, // Override for this tour only
},
},
} satisfies GuidonToursConfig;GuidonStep
interface GuidonStep {
id: string; // Unique step identifier
targetId?: string; // ID of target element (optional for floating steps)
title: string; // Tooltip title
description: string; // Tooltip description
tooltipPosition?: 'top' | 'bottom' | 'left' | 'right' | 'auto';
floatingPosition?: 'center' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
customContent?: ReactNode; // Additional content in tooltip
onStepEnter?: () => void; // Called when step becomes active
onStepExit?: () => void; // Called when leaving step
waitingMessage?: string; // Message while waiting for target to mount
waitTimeout?: number; // Auto-skip timeout (ms) if target doesn't mount
}GuidonTheme
interface GuidonTheme {
backdropColor?: string; // Overlay color (default: '#000000')
backdropOpacity?: number; // Overlay opacity (default: 0.5)
tooltipBackgroundColor?: string;
tooltipBorderColor?: string;
tooltipBorderRadius?: number;
titleColor?: string;
descriptionColor?: string;
primaryColor?: string; // Button color
mutedColor?: string; // Secondary text color
spotlightBorderRadius?: number; // Spotlight cutout radius
spotlightPadding?: number; // Padding around spotlight
tooltipDraggable?: boolean; // Enable draggable tooltips (default: false)
dragHintText?: string; // Hint text shown on first step (default: 'Drag to move')
}Controlling the Guidon
Using the Context Hook
import { useGuidonContext } from '@korsolutions/guidon';
function ReplayButton() {
const { replay, isCompleted, start } = useGuidonContext();
return (
<>
{isCompleted && (
<Button onPress={() => replay('explore')}>Replay Tutorial</Button>
)}
<Button onPress={() => start('explore')}>Start Explore Tour</Button>
</>
);
}Using Hook Selectors
import {
useGuidonActive,
useGuidonStep,
useGuidonProgress,
} from '@korsolutions/guidon';
function GuidonStatus() {
const isActive = useGuidonActive();
const currentStep = useGuidonStep();
const { currentStep: stepNum, totalSteps, percentage } = useGuidonProgress();
if (!isActive) return null;
return (
<Text>
Step {stepNum} of {totalSteps} ({percentage}%)
</Text>
);
}Custom Tooltip Rendering
<GuidonProvider
renderTooltip={({ step, currentIndex, totalSteps, onNext, onPrevious, onSkip }) => (
<View style={styles.customTooltip}>
<Text style={styles.title}>{step.title}</Text>
<Text>{step.description}</Text>
<View style={styles.buttons}>
<Button onPress={onSkip}>Skip</Button>
{currentIndex > 0 && <Button onPress={onPrevious}>Back</Button>}
<Button onPress={onNext}>
{currentIndex === totalSteps - 1 ? 'Done' : 'Next'}
</Button>
</View>
</View>
)}
>Conditional Starting
<GuidonProvider
autoStart="welcome"
shouldStart={async () => {
// Check if user is new
const user = await getUser();
return user.isFirstLogin;
}}
>Auto-Scroll Behavior
Web
On web, targets automatically scroll into view using the native scrollIntoView() API. You can customize this via theme:
const config = {
theme: {
scrollOptions: {
behavior: 'smooth', // 'smooth' | 'instant'
block: 'center', // 'start' | 'center' | 'end' | 'nearest'
inline: 'nearest', // 'start' | 'center' | 'end' | 'nearest'
},
},
tours: { ... },
} satisfies GuidonToursConfig;Mobile (React Native)
For mobile, use the useGuidonScrollContainer hook to enable auto-scrolling:
import { useRef } from 'react';
import { ScrollView } from 'react-native';
import { useGuidon, useGuidonScrollContainer } from '@korsolutions/guidon';
function MyScreen() {
const scrollRef = useRef<ScrollView>(null);
const { register } = useGuidon();
// Enable auto-scroll when steps change
useGuidonScrollContainer(scrollRef);
return (
<ScrollView ref={scrollRef}>
<View ref={register('step-1')}>First target</View>
<View ref={register('step-2')}>Second target</View>
</ScrollView>
);
}Options:
useGuidonScrollContainer(scrollRef, {
padding: 150, // Distance from top of viewport (default: 100)
enabled: false, // Temporarily disable (default: true)
});Disabling Scroll Per Step
Disable auto-scroll for individual steps:
{
id: 'modal-step',
targetId: 'modal-button',
title: 'Modal Content',
description: 'This element is already visible',
scrollIntoView: false, // Don't scroll for this step
}Platform Support
- iOS (New Architecture / Fabric supported)
- Android (New Architecture / Fabric supported)
- Web (React Native Web)
Requirements
- React Native 0.71+ (for New Architecture support)
- React 18+ (for
useSyncExternalStore)
License
MIT
