@wrack/react-native-tour-guide
v0.1.4
Published
Integrate a powerful and customizable React Native app tour guide/walkthrough and coach marks in less then 1 minute. Packed with advanced features like SVG masking, smooth animations, fully customizable tooltips, automatic scrolling, light/dark mode suppo
Maintainers
Keywords
Readme
React Native Tour Guide
A lightweight React Native library for building app tours, walkthroughs, and coach marks. The spotlight automatically matches your component's shape — circles stay circular, pills stay pill-shaped, no manual configuration needed.
Works with Expo, React Native CLI, and React Native Web. Zero native dependencies. New Architecture ready.
What does this library do?
It creates an overlay that highlights specific components in your app and shows tooltips explaining each one. You define a list of steps (which component to highlight, what to say), call startTour(), and the library handles everything else — measuring elements, positioning tooltips, scrolling to off-screen targets, animating between steps, and matching the spotlight shape to each component's border radius.
How is it different from other tour libraries?
| Capability | This library | Alternatives |
|---|---|---|
| Auto shape matching | Spotlight matches target's border radius automatically | Manual shape selection |
| Auto-scroll | Scrolls to ensure target + tooltip both fit on screen | Manual or none |
| Smart tooltip positioning | Auto-detects best position, never renders off-screen | Manual per step |
| Theme presets | 4 built-in + createTheme() API | None |
| Per-corner border radius | Extracted automatically from targetStyle | Not supported |
| Pulse animation | Configurable animated spotlight border | Not supported |
| Pause/Resume | Built-in state preservation | Not supported |
| Tour persistence | Built-in "show only once" hook | DIY |
| Conditional steps | active flag with auto-renumbering | Filter manually |
| Bundle size | < 50KB, zero native dependencies | > 200KB |
| Web support | Full react-native-web support | Not supported |
Installation
npm install @wrack/react-native-tour-guide react-native-svgOr with Expo:
npx expo install @wrack/react-native-tour-guide react-native-svgAlso works with yarn add and pnpm add.
Optional dependencies
These are only needed for enhanced visual effects:
# Blur effect
npm install @react-native-community/blur
# Gradient overlay
npm install react-native-linear-gradient
# Advanced blur masking
npm install @react-native-masked-view/masked-viewQuick start
1. Wrap your app
import { TourGuideProvider, TourGuideOverlay } from '@wrack/react-native-tour-guide';
export default function App() {
return (
<TourGuideProvider>
<YourApp />
<TourGuideOverlay />
</TourGuideProvider>
);
}2. Start a tour
import { useRef } from 'react';
import { View, Text, Pressable } from 'react-native';
import { useTourGuide } from '@wrack/react-native-tour-guide';
function HomeScreen() {
const { startTour } = useTourGuide();
const buttonRef = useRef(null);
const avatarRef = useRef(null);
const handleStart = () => {
startTour([
{
id: 'welcome',
targetRef: buttonRef,
title: 'Welcome',
description: 'Tap here to get started.',
targetStyle: styles.button, // spotlight matches button's border radius
},
{
id: 'avatar',
targetRef: avatarRef,
title: 'Your Profile',
description: 'Tap to view your profile.',
targetStyle: styles.avatar, // circular target gets circular spotlight
},
]);
};
return (
<View>
<Pressable ref={buttonRef} style={styles.button} onPress={handleStart}>
<Text>Start Tour</Text>
</Pressable>
<View ref={avatarRef} style={styles.avatar} />
</View>
);
}
const styles = StyleSheet.create({
button: { borderRadius: 12, padding: 16, backgroundColor: '#007AFF' },
avatar: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#ccc' },
});That's it. The spotlight around the button will have 12px rounded corners. The spotlight around the avatar will be a perfect circle.
How does auto shape matching work?
Pass the same style you use on the component as targetStyle on the step. The library reads the borderRadius properties from that style and applies them to the spotlight.
- Fully rounded elements (where
borderRadius >= min(width, height) / 2) stay fully rounded even when the spotlight is slightly larger - Per-corner radii (
borderTopLeftRadius, etc.) are preserved exactly — the spotlight matches the asymmetric shape - Partial radii (like
borderRadius: 12on a card) stay at exactly 12px on the spotlight
// Chat bubble with asymmetric corners
const chatStyle = {
borderTopLeftRadius: 16,
borderTopRightRadius: 16,
borderBottomRightRadius: 16,
borderBottomLeftRadius: 4,
};
{ id: 'chat', targetRef: chatRef, targetStyle: chatStyle, title: '...', description: '...' }
// Spotlight will have 16px on top-left, top-right, bottom-right and 4px on bottom-leftYou can also override the border radius manually:
// Uniform override
{ id: 'step', targetRef: ref, spotlightBorderRadius: 20, title: '...', description: '...' }Priority: spotlightBorderRadius > auto-extracted from targetStyle > default (12px).
How to enable auto-scroll?
Set scrollRef on the tour config. The library automatically scrolls to ensure both the target and its tooltip are fully visible.
const scrollViewRef = useRef(null);
const [scrollY, setScrollY] = useState(0);
startTour(steps, {
scrollRef: scrollViewRef,
getCurrentScrollOffset: () => scrollY,
});
// In your JSX:
<ScrollView
ref={scrollViewRef}
onScroll={(e) => setScrollY(e.nativeEvent.contentOffset.y)}
scrollEventThrottle={16}
>
{/* your content */}
</ScrollView>You can also set scrollToTarget per step for fine-grained control:
{
id: 'far-down',
targetRef: ref,
title: 'Scroll Target',
description: '...',
scrollToTarget: {
scrollRef: scrollViewRef,
offset: 50,
animated: true,
getCurrentScrollOffset: () => scrollY,
},
}How to use themes?
Four built-in themes are included. Spread them into your config:
import { darkTheme, lightTheme, minimalTheme, vibrantTheme } from '@wrack/react-native-tour-guide';
startTour(steps, { ...darkTheme });Create a custom theme with createTheme():
import { createTheme } from '@wrack/react-native-tour-guide';
const brandTheme = createTheme({
tooltipStyles: { primaryButtonColor: '#FF6B35', backgroundColor: '#1B1B3A' },
spotlightStyles: { overlayOpacity: 0.7 },
});
startTour(steps, { ...brandTheme });How to add pulse animation?
startTour(steps, {
spotlightStyles: {
enablePulse: true,
pulseColor: '#00BFFF',
pulseWidth: 3,
pulseDuration: 1200,
pulseMinOpacity: 0.3,
pulseMaxOpacity: 0.9,
},
});How to show a tour only once?
Use the useTourPersistence hook with any storage backend:
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useTourPersistence } from '@wrack/react-native-tour-guide';
function MyScreen() {
const { startTour, resetTour } = useTourPersistence(AsyncStorage);
useEffect(() => {
// Automatically skips if user already completed this tour
startTour(steps, { tourId: 'onboarding' });
}, []);
// Force show again
const showAgain = async () => {
await resetTour('onboarding');
startTour(steps, { tourId: 'onboarding' }, true);
};
}Works with MMKV too:
import { MMKV } from 'react-native-mmkv';
const storage = new MMKV();
const adapter = {
getItem: (key) => storage.getString(key) ?? null,
setItem: (key, value) => storage.set(key, value),
removeItem: (key) => storage.delete(key),
};
const { startTour } = useTourPersistence(adapter);How to use conditional steps?
Set active: false to hide a step. Numbering adjusts automatically.
const isPremium = useIsPremium();
startTour([
{ id: 'welcome', targetRef: welcomeRef, title: 'Welcome', description: '...' },
{
id: 'upgrade',
targetRef: upgradeRef,
title: 'Upgrade',
description: 'Only shown to free users.',
active: !isPremium,
},
{ id: 'done', targetRef: doneRef, title: 'All Set', description: '...' },
]);How to handle backdrop taps?
startTour([
{
id: 'step1',
targetRef: ref,
title: 'Tap anywhere',
description: 'Tapping the dark overlay advances to the next step.',
backdropBehavior: 'next', // 'dismiss' | 'next' | 'none' | () => void
},
]);
// Or set globally
startTour(steps, { defaultBackdropBehavior: 'next' });How to pause and resume a tour?
const { pauseTour, resumeTour, isPaused } = useTourGuide();
// Pause when opening a modal
const handleModalOpen = () => pauseTour();
// Resume when it closes — picks up where it left off
const handleModalClose = () => resumeTour();How to use a custom tooltip?
import type { TooltipProps } from '@wrack/react-native-tour-guide';
const MyTooltip = (props: TooltipProps) => (
<View style={myStyles.tooltip}>
<Text>{props.title}</Text>
<Text>{props.description}</Text>
<Pressable onPress={props.onNext}><Text>Continue</Text></Pressable>
</View>
);
startTour(steps, {
renderTooltip: (props) => <MyTooltip {...props} />,
});Programmatic control
const {
startTour, nextStep, prevStep, skipTour, endTour,
goToStep, pauseTour, resumeTour,
isActive, isPaused, currentStep, activeTourId,
} = useTourGuide();API reference
TourStep
| Property | Type | Default | Description |
|---|---|---|---|
| id | string | required | Unique step identifier |
| targetRef | RefObject | — | Ref to the component to highlight |
| title | string | required | Tooltip title |
| description | string | required | Tooltip body text |
| tooltipPosition | 'top' \| 'bottom' \| 'left' \| 'right' \| 'auto' | 'auto' | Tooltip placement (auto-detected by default) |
| targetStyle | ViewStyle | — | Style to extract border radius from for shape matching |
| spotlightPadding | number | 0 | Extra padding around the spotlight |
| spotlightBorderRadius | number | — | Override border radius (takes priority over targetStyle) |
| scrollToTarget | ScrollToTargetConfig | — | Per-step scroll configuration |
| active | boolean | true | Whether this step is included |
| backdropBehavior | BackdropBehavior | 'none' | What happens on backdrop tap |
| autoAdvance | number | 0 | Auto-advance after ms (0 = disabled) |
| delayBefore | number | 0 | Delay before showing step |
| onNext | () => void | — | Called on next |
| onPrev | () => void | — | Called on previous |
| onSkip | () => void | — | Called on skip |
| onSpotlightPress | () => void | — | Called when spotlight area is tapped |
| hideNextButton | boolean | false | Hide the next/done button |
| hidePrevButton | boolean | false | Hide the back button |
| hideSkipButton | boolean | false | Hide the skip button |
| accessibilityLabel | string | — | Custom screen reader label |
TourGuideConfig
| Property | Type | Default | Description |
|---|---|---|---|
| tooltipStyles | TooltipStyles | — | Tooltip appearance |
| spotlightStyles | SpotlightStyles | — | Spotlight/overlay appearance |
| scrollRef | RefObject | — | ScrollView ref for auto-scrolling |
| getCurrentScrollOffset | () => number | — | Returns current scroll Y position |
| showProgressDots | boolean | false | Show dot indicators |
| showStepCounter | boolean | true | Show "1/5" counter |
| enableBackButton | boolean | true | Show back button |
| nextButtonText | string | 'Next' | Next button label |
| prevButtonText | string | 'Back' | Back button label |
| skipButtonText | string | 'Skip' | Skip button label |
| doneButtonText | string | 'Done' | Done button label |
| animationDuration | number | 300 | Transition duration (ms) |
| tooltipWidth | number | 320 | Tooltip width (px) |
| tourId | string | — | Tour identifier (for persistence) |
| autoPositionTooltip | boolean | — | Enable smart positioning |
| defaultBackdropBehavior | BackdropBehavior | 'none' | Global backdrop behavior |
| renderTooltip | (props) => ReactNode | — | Custom tooltip renderer |
| onTourStart | () => void | — | Called when tour starts |
| onTourEnd | (completed: boolean) => void | — | Called when tour ends |
| onStepChange | (from, to) => void | — | Called on step change |
| beforeStepChange | (from, to) => boolean \| Promise<boolean> | — | Gate before step change |
| enableAccessibility | boolean | true | Enable screen reader announcements |
SpotlightStyles
| Property | Type | Default | Description |
|---|---|---|---|
| overlayOpacity | number | 0.6 | Overlay darkness (0-1) |
| overlayColor | string | 'black' | Overlay color |
| enableBlur | boolean | false | Blur effect (requires optional dep) |
| blurAmount | number | 4 | Blur intensity |
| enableGradient | boolean | false | Gradient overlay |
| gradientColors | string[] | — | Gradient color stops |
| enablePulse | boolean | false | Pulsing spotlight border |
| pulseColor | string | '#FFFFFF' | Pulse color |
| pulseWidth | number | 2 | Pulse border width (px) |
| pulseDuration | number | 1500 | Full pulse cycle (ms) |
| pulseMinOpacity | number | 0.2 | Pulse min opacity |
| pulseMaxOpacity | number | 0.8 | Pulse max opacity |
TooltipStyles
| Property | Type | Default | Description |
|---|---|---|---|
| backgroundColor | string | '#2C2C2E' | Tooltip background |
| borderRadius | number | 16 | Tooltip corner radius |
| titleColor | string | '#FFFFFF' | Title text color |
| descriptionColor | string | '#FFFFFF' | Description text color |
| buttonTextColor | string | '#FFFFFF' | Button text color |
| primaryButtonColor | string | '#007AFF' | Next/Done button background |
| secondaryButtonColor | string | '#3A3A3C' | Back button background |
| skipButtonColor | string | '#FFFFFF' | Skip button color |
React Native Web
Works out of the box with react-native-web. No additional configuration.
npm install @wrack/react-native-tour-guide react-native-svg react-native-webElement measurement uses getBoundingClientRect on web. Blur and gradient effects gracefully degrade to the standard overlay since they require native modules.
Troubleshooting
Tour not showing?
- Ensure
TourGuideOverlayis placed after your main content inside the provider - Check that the target ref is attached to a mounted component
- Add
delayBefore: 500if the component needs time to render
Spotlight position wrong?
- The component must be visible on screen when measured
- Check for transforms or absolute positioning that might affect measurement
- Use
scrollRefon the config to ensure off-screen elements are scrolled into view
Tooltip overlapping target?
- Set
tooltipPosition: 'auto'(default) for automatic placement - The library checks available space on all sides and picks the best position
Best practices
- Keep tours short — 3-7 steps for best engagement
- Always allow skipping — never trap users
- Use
targetStyle— let the spotlight match automatically instead of manual configuration - Set
scrollRefglobally — handles all scroll scenarios without per-step config - Use
tourIdwithuseTourPersistence— show tours only once - Use
delayBeforeinstead ofsetTimeout— the library handles timing - Use
activefor conditional steps — numbering adjusts automatically - Test with VoiceOver/TalkBack — accessibility is enabled by default
Contributing
See CONTRIBUTING.md for development setup.
License
MIT — see LICENSE.
Created by Himanshu Lal
