npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wrack/react-native-tour-guide

v2.0.0

Published

React Native tour guide, walkthrough, and coach marks library. Auto shape-matching spotlight, smart auto-scroll, themeable tooltips, pulse animation, and a declarative API. Works with Expo and React Native CLI — zero native dependencies, TypeScript, New A

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 and React Native CLI. Zero native dependencies. New Architecture ready.

npm version npm downloads license platforms PRs welcome good first issues documentation

📚 Documentation & guides · 📦 Install from npm


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 |


Installation

# npm
npm install @wrack/react-native-tour-guide react-native-svg

# yarn
yarn add @wrack/react-native-tour-guide react-native-svg

# pnpm
pnpm add @wrack/react-native-tour-guide react-native-svg

# Expo (managed or bare)
npx expo install @wrack/react-native-tour-guide react-native-svg

Requirements

| | Minimum | |---|---| | react-native | 0.71 | | react | 18 | | react-native-svg | 13 | | react-native-web (web only) | 0.19 |

Works on the old architecture and the New Architecture (Fabric), on iOS, Android and web. react-native-safe-area-context is optional — when installed, safe-area insets are detected automatically.

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

Quick 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.


Declarative targeting with TourTarget

Skip ref-threading entirely: wrap any element in a TourTarget and reference it from steps by id. The wrapper registers itself with the provider (and uses collapsable={false} so Android's view flattening can never make the target unmeasurable).

import { TourTarget, useTourGuide } from '@wrack/react-native-tour-guide';

<TourTarget id="compose" style={{ borderRadius: 24 }}>
  <ComposeButton />
</TourTarget>;

// Steps reference the id — no refs anywhere:
startTour([
  { id: 'step1', targetId: 'compose', title: 'Compose', description: 'Start a new message.' },
]);

The style on TourTarget drives spotlight shape matching, exactly like a step's targetStyle. If the target mounts after the tour starts (a screen you navigate to), the tour waits briefly for it to register before falling back to a centered tooltip. targetRef still works everywhere and wins when both are given.


Interactive steps — let users tap the real thing

By default the overlay uses a Modal, which swallows every touch. Switch to the inline overlay and mark a step interactive to let touches pass through the spotlight hole to the actual element underneath:

startTour(
  [
    {
      id: 'send',
      targetId: 'send-button',
      title: 'Try it now',
      description: 'Tap Send to continue.',
      interactive: true,   // touches reach the real button
      hideNextButton: true,
      completed: false,    // Next stays disabled until you say so
    },
  ],
  { overlayMode: 'inline' }
);

// In the button's own handler:
const { nextStep, setStepCompleted } = useTourGuide();
const onSend = () => {
  setStepCompleted('send', true);
  nextStep();
};
  • overlayMode: 'inline' renders the overlay as an absolutely-positioned layer instead of a Modal. Put <TourGuideOverlay /> at the root of your app so it covers the full screen. (The Android back button is not intercepted in this mode.)
  • completed: false disables the Next button until setStepCompleted(stepId, true) is called — turn a slideshow into a guided task.

Async step preparation with before()

Navigate, open a sheet, or wait for data before a step is measured — the spotlight only appears once the promise resolves. Prefer this over guessing a delayBefore:

{
  id: 'profile-tab',
  targetId: 'profile-header',
  title: 'Your profile',
  description: 'Everything about you lives here.',
  before: async () => {
    navigation.navigate('Profile');
    await waitForProfileToLoad();
  },
}

If before() throws or rejects, the tour warns (dev only) and shows the step anyway rather than trapping the user.


Named tours and canStartTour

Register tours up front and start them by id from anywhere — useful when steps and triggers live in different parts of the app:

const { defineTour, startTour, canStartTour } = useTourGuide();

// At setup time (e.g. app root):
defineTour('onboarding', onboardingSteps, { showProgressDots: true });
defineTour('power-features', powerSteps);

// Anywhere else:
startTour('onboarding');                       // stored config
startTour('onboarding', { motion: 'bounce' }); // with overrides

// Wait until every <TourTarget> the tour references has mounted:
if (canStartTour('onboarding')) startTour('onboarding');

canStartTour also accepts a raw steps array. Steps with targetRef, targetRegion, or no target always count as ready.


Highlighting a region without a ref

Maps, camera previews, canvases — anything you can't attach a ref to — can be highlighted as a fixed window rectangle:

{
  id: 'map-pin',
  targetRegion: { x: 40, y: 220, width: 320, height: 180 },
  title: 'Your delivery zone',
  description: 'Everything inside this area ships free.',
}

targetRegion skips measurement entirely and wins over targetRef/targetId.


Spotlight motion presets

Choose how the spotlight travels between steps — globally or per step:

startTour(steps, { motion: 'bounce' });          // springy, with overshoot
// per step:
{ id: 'step3', targetId: 'send', motion: 'fade', ... }

| Motion | Behaviour | |---|---| | morph (default) | Position, size and radius tween in one smooth move | | bounce | The same move on a spring | | fade | The overlay dips out, jumps, and fades back in | | none | Instant jump |


Custom spotlight shapes with maskPath

The ultimate escape hatch: return any SVG subpath and it becomes the hole in the backdrop (evenodd fill). Overrides shape matching for every step:

startTour(steps, {
  spotlightStyles: {
    // A star, a blob, an arrow — anything you can path:
    maskPath: ({ bounds }) => {
      const { x, y, width: w, height: h } = bounds;
      const cx = x + w / 2;
      return `M${cx},${y} L${x + w},${y + h} L${x},${y + h} Z`; // triangle
    },
  },
});

The function receives the measured target, the padded bounds the automatic shape would use, and the screen size. Custom paths jump between steps (arbitrary paths can't be tweened reliably); a throwing maskPath falls back to the automatic shape.


Following the target while the user scrolls

Bind your scrollable to the tour with useTourScroll, and with followTarget: true the spotlight and tooltip stay glued to the target while the user scrolls freely:

import { useTourScroll } from '@wrack/react-native-tour-guide';

const { scrollProps } = useTourScroll();

<ScrollView ref={scrollRef} {...scrollProps}>…</ScrollView>;

startTour(steps, { scrollRef, followTarget: true });

Wiring useTourScroll also gives the tour two more things for free:

  • Exact auto-scroll settlingonMomentumScrollEnd tells the tour the programmatic scroll finished, so the highlight lands immediately instead of waiting for position polling to detect the stop.
  • A tracked scroll offsetgetCurrentScrollOffset becomes redundant.

Have your own onScroll? Compose it: the hook also returns the individual handlers.


Analytics with the event emitter

Subscribe once — e.g. at app root — instead of threading callbacks through every tour config:

const { events } = useTourGuide();

useEffect(() => {
  const offs = [
    events.on('start', ({ tourId, totalSteps }) => track('tour_start', { tourId, totalSteps })),
    events.on('stepChange', ({ from, to }) => track('tour_step', { from, to })),
    events.on('end', ({ completed, tourId }) => track('tour_end', { completed, tourId })),
    events.on('skip', ({ at }) => track('tour_skip', { at })),
  ];
  return () => offs.forEach((off) => off());
}, [events]);

Events: start, stepChange, end, skip, pause, resume. A throwing handler is isolated — analytics can never take the tour down. The config callbacks (onTourStart etc.) still work; both fire.


Persistence without configuration

useTourPersistence() now auto-detects storage — MMKV (v2–v4) first, then AsyncStorage:

const { startTour } = useTourPersistence(); // no adapter needed
startTour(steps, { tourId: 'onboarding' });

With neither installed it falls back to in-memory storage (the tour works, completion just isn't remembered across restarts) and says so in a dev warning. Passing your own adapter still works and always wins.


Keeping earlier spotlights lit

keepSpotlight: true on a step keeps its hole punched out of the backdrop after the tour moves forward past it — for "these three things work together" storytelling:

startTour([
  { id: 'a', targetId: 'search', keepSpotlight: true, ... },
  { id: 'b', targetId: 'filters', keepSpotlight: true, ... },
  { id: 'c', targetId: 'results', title: 'Together…', ... }, // all three lit
]);

Kept holes are visual only (no pulse, no touch pass-through) and are dropped when the user navigates back.


Per-platform values

The layout-tuning config fields accept { ios, android, web, default } objects:

startTour(steps, {
  tooltipWidth: { ios: 320, android: 300, default: 320 },
  motion: { ios: 'bounce', default: 'morph' },
});

Applies to tooltipWidth, tooltipOffset, triangleSize, animationDuration, safeZoneOffset, and motion. Values are resolved once at startTour; everything you read back from context is already plain.


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: 12 on 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-left

You 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. getCurrentScrollOffset is optional — without it the destination is derived from the target's position inside the scroll content (measureLayout), which needs no offset tracking at all. Pass the getter only if you want the offset-based calculation.

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>

This works with ScrollView, FlatList, SectionList, Animated.ScrollView and gesture-handler wrappers — the library detects each one's scroll method, so you pass the ref the same way regardless:

const listRef = useRef(null);
const [scrollY, setScrollY] = useState(0);

startTour(steps, {
  scrollRef: listRef,              // FlatList works exactly like ScrollView
  getCurrentScrollOffset: () => scrollY,
});

<FlatList
  ref={listRef}
  onScroll={(e) => setScrollY(e.nativeEvent.contentOffset.y)}
  scrollEventThrottle={16}
  data={data}
  renderItem={renderItem}
/>

Attach the ref to the list itself, not to a wrapping View. If the ref has no scroll method the tour warns in development and highlights the target where it is, rather than scrolling.

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 | | targetId | string | — | Id of a <TourTarget> to highlight — the ref-free alternative | | targetRegion | {x, y, width, height} | — | Highlight a fixed window rectangle (no ref needed); wins over targetRef/targetId | | 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 \| {top,right,bottom,left} | 0 | Padding around the spotlight — uniform or per-side | | 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) | | before | () => void \| Promise<void> | — | Awaited before the step is measured — navigate/fetch here | | delayBefore | number | 0 | Delay before showing step | | completed | boolean | — | false disables Next until setStepCompleted(id, true) | | interactive | boolean | false | Let touches through the spotlight to the real element (inline mode) | | renderTooltip | (props) => ReactNode | — | Per-step custom tooltip (overrides the config one) | | motion | 'morph' \| 'bounce' \| 'fade' \| 'none' | config | Spotlight transition into this step | | keepSpotlight | boolean | false | Keep this hole punched out after moving forward past the step | | waitForInteractions | boolean | config | Wait for InteractionManager before measuring this 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 | — | Ref to the ScrollView, FlatList or SectionList to auto-scroll | | getCurrentScrollOffset | () => number | — | Optional scroll-offset getter; without it the destination is derived via measureLayout | | overlayMode | 'modal' \| 'inline' | 'modal' | 'inline' renders without a Modal — required for interactive steps | | components | TooltipComponents | — | Replace individual tooltip pieces (NextButton, SkipButton, dots…) | | allowFontScaling | boolean | true | Tooltip text follows the OS font-size setting | | maxFontSizeMultiplier | number | — | Cap on OS font scaling for tooltip text | | 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) | | motion | 'morph' \| 'bounce' \| 'fade' \| 'none' | 'morph' | Spotlight transition between steps (accepts per-platform values) | | followTarget | boolean | false | Highlight tracks the target during free scrolling (needs useTourScroll) | | waitForInteractions | boolean | false | Wait for navigation/layout animations before measuring each step | | statusBarStyle | 'light-content' \| 'dark-content' \| 'auto' | — | Status-bar style while the tour runs (restored after) | | supportedOrientations | Orientation[] | all | iOS Modal orientations (default: rotate with the app) | | tooltipWidth | number | 320 | Tooltip width (px) | | tourId | string | — | Tour identifier (for persistence) | | autoPositionTooltip | boolean | true | Flip the tooltip to whichever side has room. Set false to always honour each step's tooltipPosition | | 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. Return false to block it. On the final step to is totalSteps (one past the end), so to > from stays true when finishing | | 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 | | maskPath | (args) => string | — | Custom SVG subpath for the spotlight hole — draw any shape | | 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 | | arrowStyle | ViewStyle | — | Style merged onto the tooltip arrow/triangle | | 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 |


Troubleshooting

In development the library validates your steps and config on every startTour() and prints a warning naming the exact step and the fix. All warnings are stripped from production builds, so check the Metro logs first — the answer is usually there. Common cases:

Tour not showing at all?

  • Render <TourGuideOverlay /> exactly once, inside <TourGuideProvider> and after your main content. Calling startTour() with no overlay mounted warns: "the tour is running invisibly".
  • Check that every step is not active: false — a tour with no active steps does not start.
  • Add delayBefore: 500 to a step whose component needs time to render.

Spotlight in the wrong place, or a centered tooltip instead of a spotlight?

  • The target must be a React Native host component (View, Text, Pressable…). A custom component must forward the ref with React.forwardRef, or there is nothing to measure and the step falls back to a centered tooltip.
  • Pass the ref itself (targetRef={myRef}), not myRef.current and not a callback ref.
  • The component must be laid out when measured — use scrollRef so off-screen targets are scrolled into view.

Auto-scroll not working?

  • Attach scrollRef to the scrollable itself, not a wrapper View.
  • Pass getCurrentScrollOffset as well. Without it the tour assumes the list sits at offset 0 and scrolls to the wrong place once the user has scrolled.

Tooltip overlapping the target or running off-screen?

  • Leave autoPositionTooltip at its default (true) so the tooltip flips to whichever side has room.
  • Pass insets (from useSafeAreaInsets()) and extraInsets for tab bars and headers so the tooltip stays clear of system chrome.

The Done button does nothing on the last step?

  • If you use beforeStepChange, remember to is totalSteps on the final step. A guard that only allows known indices will block finishing.

Nothing happens when I tap Next?

  • A beforeStepChange promise that never settles holds the transition lock. Make sure it always resolves.

Best practices

  1. Keep tours short — 3-7 steps for best engagement
  2. Always allow skipping — never trap users
  3. Use targetStyle — let the spotlight match automatically instead of manual configuration
  4. Set scrollRef globally — handles all scroll scenarios without per-step config
  5. Use tourId with useTourPersistence — show tours only once
  6. Use delayBefore instead of setTimeout — the library handles timing
  7. Use active for conditional steps — numbering adjusts automatically
  8. Test with VoiceOver/TalkBack — accessibility is enabled by default

Frequently asked questions

How do I add an onboarding tour to a React Native app?

Install @wrack/react-native-tour-guide and react-native-svg, wrap your app in TourGuideProvider with a TourGuideOverlay, then call startTour(steps) from the useTourGuide() hook. Each step references the component to highlight (via a ref) and the title/description to show. See Quick start.

How do I highlight a specific component with a spotlight?

Attach a ref to the component and pass it as targetRef on a step. The library measures the component, dims the rest of the screen, and cuts out a spotlight that matches the component's border radius automatically. Pass the component's style as targetStyle so the spotlight matches its shape.

Can I fully customize the tooltip?

Yes. Use config.tooltipStyles to restyle the built-in tooltip, or pass config.renderTooltip to render your own component — it receives the title, description, step index, and onNext/onPrev/onSkip handlers. See How to use a custom tooltip?.

Does it work with Expo?

Yes. It works with Expo (managed and bare) and React Native CLI. The only required dependency is react-native-svg, which Expo supports out of the box.

Does it support the New Architecture (Fabric)?

Yes. The dark overlay is drawn with a single even-odd SVG path (a real punched-out hole) rather than an SVG <Mask>, so the spotlight renders correctly on both the old architecture and Fabric, with no white film over the highlighted element.

Does it work with ScrollView, FlatList, and SectionList?

Yes. Pass a scrollRef (and a getCurrentScrollOffset getter) on the config and the tour scrolls off-screen targets into view automatically, keeping both the target and its tooltip on screen. Each scrollable exposes a different imperative API — scrollTo on ScrollView, scrollToOffset on FlatList, a scroll responder on SectionList — and the library detects the right one, including Animated.ScrollView and gesture-handler wrappers.

What are the dependencies and bundle size?

The library itself is under 50KB and has zero native dependencies — only react-native-svg as a peer. Blur and gradient effects are fully optional and load lazily only if you install them, degrading gracefully to the standard overlay otherwise.

Does it work with Jest?

Yes, with no extra configuration. The package ships both a CommonJS and an ES module build with the right require/import export conditions, so it resolves correctly under the react-native Jest preset without adding it to transformIgnorePatterns.

Is it written in TypeScript?

Yes — it ships with full TypeScript types for every step option, config field, and theme.

How does it compare to react-native-copilot or rn-tourguide?

All three highlight UI elements with tooltips. This library additionally matches the spotlight shape to each target's border radius automatically (circles, pills, per-corner radii), auto-scrolls so the target and tooltip both fit on screen, and ships zero native dependencies — so it runs in Expo Go without a custom dev build.

Can I show a tour only once per user?

Yes — use the useTourPersistence hook with any storage backend (AsyncStorage, MMKV, or a custom adapter). See How to show a tour only once?.


Roadmap & help wanted

Contributions are welcome — issues and pull requests of any size help. Good places to start:

  • New features and enhancements
  • More built-in themes
  • Additional examples (drawer/tab navigators, modals, lists)
  • Documentation improvements and tests
  • Bug fixes of any size

Browse good first issues or open a discussion.


Installing from GitHub Packages (mirror)

This package is also mirrored to GitHub Packages as @himanshu-lal4/react-native-tour-guide. Note that GitHub Packages requires authentication even for public packages, so installing from npm (see Installation) is recommended for most users.

  1. Create a GitHub personal access token with the read:packages scope.
  2. Add the scope routing and token to your project's .npmrc:
@himanshu-lal4:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=YOUR_GITHUB_TOKEN
  1. Install (only the @himanshu-lal4 scope routes to GitHub — all other dependencies still come from npm):
npm install @himanshu-lal4/react-native-tour-guide react-native-svg
  1. Import from the mirrored name:
import { TourGuideProvider } from '@himanshu-lal4/react-native-tour-guide';

Contributing & community

Issues, ideas, and pull requests of every size are welcome — bug reports and docs improvements help just as much as features.

New to the project? Read CONTRIBUTING.md for local setup, the project structure, and the development workflow.

Also by the same author

License

MIT — see LICENSE.


Created by Himanshu Lal