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

@decaylabs/react-native-intro

v0.1.4

Published

A React Native implementation of intro.js for mobile app onboarding

Readme

react-native-intro

A native React Native implementation of intro.js — step-by-step user onboarding tours and contextual hints for mobile apps. This is not a wrapper around the web library; it's a complete reimplementation with feature parity to intro.js v8.0.0+.

Features

  • Step-by-step tours with spotlight overlay and smooth animations
  • Contextual hints with pulsing indicators and tap-to-reveal tooltips
  • Props-based or programmatic configuration
  • Rich tooltip content — titles, images, custom React components
  • Floating tooltips for welcome/intro screens (no target element)
  • Auto-scroll to off-screen elements
  • Smart positioning with automatic edge detection
  • Progress indicators — progress bar and step bullets
  • Theming — built-in themes (classic, modern, dark, auto) + custom themes
  • Persistence — "Don't show again" with AsyncStorage
  • Accessibility — VoiceOver/TalkBack, screen reader announcements, reduced motion
  • TypeScript — full type definitions included

Installation

npm install react-native-intro
# or
yarn add react-native-intro
# or
pnpm add react-native-intro

Required Peer Dependencies

# Smooth animations (required)
npm install react-native-reanimated

Configure the Reanimated babel plugin in your babel.config.js:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['react-native-reanimated/plugin'], // Must be last
};

Optional Dependencies

# "Don't show again" persistence
npm install @react-native-async-storage/async-storage

Expo Users

Use npx expo install to automatically pick compatible versions for your SDK:

npx expo install react-native-intro react-native-reanimated @react-native-async-storage/async-storage

Quick Start

1. Wrap Your App with IntroProvider

import { IntroProvider } from '@decaylabs/react-native-intro';

export default function App() {
  return (
    <IntroProvider>
      <MyApp />
    </IntroProvider>
  );
}

2. Mark Elements as Tour Steps

import { TourStep, useTour } from '@decaylabs/react-native-intro';
import { View, Text, Button } from 'react-native';

function HomeScreen() {
  const tour = useTour();

  return (
    <View>
      <TourStep id="welcome" order={1} intro="Welcome to the app!" title="Hello">
        <Text style={styles.header}>My App</Text>
      </TourStep>

      <TourStep id="profile" order={2} intro="Tap here to view your profile">
        <Button title="Profile" onPress={() => {}} />
      </TourStep>

      <TourStep id="settings" order={3} intro="Access settings here" position="left">
        <Button title="Settings" onPress={() => {}} />
      </TourStep>

      <Button title="Start Tour" onPress={() => tour.start()} />
    </View>
  );
}

3. Start the Tour

// Props-based (recommended) - uses content from TourStep props
tour.start();

// With options
tour.start({ showProgress: true, dontShowAgain: true });

// For a specific group
tour.start('onboarding');

API Reference

Components

IntroProvider

Context provider that enables tour and hint functionality. Must wrap your entire app or the portion that uses tours/hints.

<IntroProvider
  theme="classic"                    // 'classic' | 'modern' | 'dark' | 'auto' | Theme
  defaultTourOptions={{ ... }}       // TourOptions
  defaultHintOptions={{ ... }}       // HintOptions
  storageAdapter={customAdapter}     // Optional custom storage
  disablePersistence={false}         // Disable "Don't show again" persistence
>
  <App />
</IntroProvider>

TourStep

Wrapper component that registers an element as a tour step.

<TourStep
  id="unique-id"           // Required: unique identifier
  order={1}                // Step order (lower = earlier)
  intro="Step content"     // Tooltip content (string or ReactNode)
  title="Step Title"       // Optional tooltip title
  position="auto"          // Tooltip position: top, bottom, left, right, auto
  disableInteraction       // Prevent touch on element during tour
  group="tour-name"        // Group identifier for multiple tours
>
  <YourComponent />
</TourStep>

// Floating step (no element highlight) - just omit children
<TourStep
  id="welcome"
  order={1}
  title="Welcome!"
  intro="This tooltip appears centered on screen."
/>

Tooltip Positions: top, bottom, left, right, auto

Floating Steps: Omit children for a centered tooltip without highlighting any element. Useful for welcome messages.

HintSpot

Wrapper component that registers an element as a hint anchor.

<HintSpot
  id="unique-id"              // Required: unique identifier
  hint="Hint content"         // Hint tooltip content
  hintPosition="top-right"    // Indicator position
  hintAnimation={true}        // Pulsing animation
  hintType="default"          // 'default' | 'info' | 'warning' | 'error' | 'success'
>
  <YourComponent />
</HintSpot>

Hint Positions: top-left, top-center, top-right, middle-left, middle-right, bottom-left, bottom-center, bottom-right

Hooks

useIntro

Combined hook providing both tour and hint controls.

const { tour, hints, callbacks } = useIntro();

// Tour state
tour.isActive           // boolean
tour.tourId             // string | null
tour.currentStep        // number (0-based)
tour.totalSteps         // number
tour.currentStepConfig  // StepConfig | null
tour.isTransitioning    // boolean

// Tour controls
tour.start()                    // Start props-based tour
tour.start(options)             // With options
tour.start('tour-id')           // Specific group
tour.start('id', steps)         // Programmatic with steps
tour.start('id', steps, opts)   // With steps and options
tour.next()                     // Next step
tour.prev()                     // Previous step
tour.goTo(2)                    // Jump to step index
tour.stop()                     // End tour
tour.restart()                  // Restart from beginning
tour.isDismissed('tour-id')     // Check if permanently dismissed
tour.clearDismissed('tour-id')  // Clear dismissed state
tour.refresh()                  // Re-measure elements

// Hints state
hints.isVisible         // boolean
hints.activeHintId      // string | null
hints.hints             // HintConfig[]

// Hints controls
hints.show()                  // Show props-based hints
hints.show(options)           // With options
hints.show(configs)           // Programmatic with configs
hints.show(configs, options)  // With configs and options
hints.hide()                  // Hide all hints
hints.showHint('id')          // Show specific hint tooltip
hints.hideHint('id')          // Hide specific hint tooltip
hints.removeHint('id')        // Remove hint entirely
hints.refresh()               // Re-measure positions

// Callbacks
callbacks.setTourCallbacks({ ... })
callbacks.setHintCallbacks({ ... })

useTour

Tour-only hook (lighter weight if you don't need hints).

const tour = useTour();
// Same API as useIntro().tour

useHints

Hints-only hook (lighter weight if you don't need tours).

const hints = useHints();
// Same API as useIntro().hints

Programmatic Tours

For dynamic content or CMS-driven tours:

const tour = useTour();

// First, wrap elements with TourStep (just the id, no content props needed)
<TourStep id="welcome"><Header /></TourStep>
<TourStep id="profile"><ProfileButton /></TourStep>

// Then start with explicit step configs
tour.start('welcome-tour', [
  {
    id: 'step-1',
    targetId: 'welcome',  // Must match a TourStep id
    title: 'Welcome!',
    content: 'Let me show you around.',
  },
  {
    id: 'step-2',
    targetId: 'profile',  // Must match a TourStep id
    content: 'Tap here to view your profile.',
    position: 'bottom',
  },
  {
    id: 'step-3',
    // No targetId = floating tooltip (centered, no spotlight)
    title: 'You\'re all set!',
    content: 'Enjoy using the app.',
  },
]);

Tour Options

interface TourOptions {
  showProgress?: boolean;        // Show progress bar (default: true)
  showBullets?: boolean;         // Show step dots (default: true)
  showButtons?: boolean;         // Show nav buttons (default: true)
  exitOnOverlayClick?: boolean;  // Close on overlay tap (default: false)
  dontShowAgain?: boolean;       // Show checkbox (default: false)
  disableInteraction?: boolean;  // Block element touch (default: false)
  scrollToElement?: boolean;     // Auto-scroll (default: true)
  scrollPadding?: number | {     // Scroll padding (default: 50)
    top?: number;
    bottom?: number;
    left?: number;
    right?: number;
  };
  overlayOpacity?: number;       // 0-1 (default: 0.75)
  overlayColor?: string;         // Overlay color
  animate?: boolean | 'auto';    // Animations (default: 'auto')
  animationDuration?: number;    // Duration ms (default: 300)
  buttonLabels?: {
    next?: string;               // default: 'Next'
    prev?: string;               // default: 'Back'
    done?: string;               // default: 'Done'
    skip?: string;               // default: 'Skip'
    dontShowAgain?: string;      // default: "Don't show again"
  };
  tooltipStyle?: ViewStyle;
  overlayStyle?: ViewStyle;
}

Hint Options

interface HintOptions {
  autoShow?: boolean;            // Show on render (default: false)
  animation?: boolean;           // Pulsing animation (default: true)
  closeOnOutsideClick?: boolean; // Close on outside tap (default: true)
  indicatorSize?: number;        // Indicator size (default: 20)
  indicatorStyle?: ViewStyle;
  tooltipStyle?: ViewStyle;
}

Callbacks

All onBefore* callbacks support both sync and async (Promise) return values. Return false to prevent the action.

const { callbacks } = useIntro();

useEffect(() => {
  callbacks.setTourCallbacks({
    // Called before tour starts (async supported)
    onBeforeStart: async (tourId) => {
      const canStart = await checkPermissions();
      return canStart; // false prevents start
    },

    // Called after tour starts
    onStart: (tourId) => {
      analytics.track('tour_started', { tourId });
    },

    // Called before step change (async supported)
    onBeforeChange: async (currentStep, nextStep, direction) => {
      if (direction === 'next' && currentStep === 1) {
        const valid = await validateForm();
        if (!valid) {
          showError('Complete the form first');
          return false;
        }
      }
      return true;
    },

    // Called after step change
    onChange: (currentStep, previousStep) => {
      console.log(`Step ${previousStep} → ${currentStep}`);
    },

    // Called before tour exit (async supported)
    onBeforeExit: async (reason) => {
      if (reason !== 'completed') {
        return await showConfirmDialog('Exit tour?');
      }
      return true;
    },

    // Called after tour ends
    onComplete: (tourId, reason) => {
      analytics.track('tour_completed', { tourId, reason });
    },
  });

  callbacks.setHintCallbacks({
    onHintsShow: () => console.log('Hints shown'),
    onHintsHide: () => console.log('Hints hidden'),
    onHintClick: (hintId) => console.log(`Hint ${hintId} clicked`),
    onHintClose: (hintId) => console.log(`Hint ${hintId} closed`),
  });
}, []);

Rich Tooltip Content

Tooltips support images and custom React components:

tour.start('tutorial', [
  {
    id: 'step-1',
    targetId: 'feature',
    title: 'New Feature!',
    content: 'Check out this amazing feature.',
    image: {
      source: require('./feature.png'), // or { uri: 'https://...' }
      width: '100%',
      height: 150,
      borderRadius: 8,
      position: 'top', // 'top' | 'bottom'
      alt: 'Feature screenshot',
    },
  },
  {
    id: 'step-2',
    targetId: 'custom',
    title: 'Custom Content',
    content: (
      <View>
        <Text>Custom React component!</Text>
        <Button title="Learn More" onPress={handleLearnMore} />
      </View>
    ),
  },
]);

Theming

Built-in Themes

<IntroProvider theme="classic">  {/* default */}
<IntroProvider theme="modern">   {/* contemporary design */}
<IntroProvider theme="dark">     {/* dark mode */}
<IntroProvider theme="auto">     {/* follows system setting */}

Custom Theme

import { IntroProvider, createTheme, mergeTheme, classicTheme } from '@decaylabs/react-native-intro';

// Full custom theme
const myTheme = createTheme({
  name: 'custom',
  overlay: {
    backgroundColor: '#000',
    opacity: 0.8,
  },
  tooltip: {
    backgroundColor: '#1a1a2e',
    borderRadius: 12,
    titleColor: '#fff',
    contentColor: '#e0e0e0',
    // ... other properties
  },
  buttons: {
    primary: {
      backgroundColor: '#4361ee',
      textColor: '#fff',
      // ...
    },
    secondary: { /* ... */ },
  },
  hint: { /* ... */ },
  progress: { /* ... */ },
});

// Or extend a built-in theme
const customTheme = mergeTheme(classicTheme, {
  overlay: { opacity: 0.9 },
  buttons: {
    primary: { backgroundColor: '#ff6b6b' },
  },
});

<IntroProvider theme={myTheme}>
  <App />
</IntroProvider>

Auto-Scroll in ScrollViews

Register your ScrollView to enable auto-scrolling to off-screen elements:

import { useScrollView } from '@decaylabs/react-native-intro';
import { ScrollView } from 'react-native';

function MyScreen() {
  const { scrollViewRef, scrollViewProps } = useScrollView();

  return (
    <ScrollView ref={scrollViewRef} {...scrollViewProps}>
      <TourStep id="step-1" intro="First element">
        <Text>Near top</Text>
      </TourStep>

      {/* ... lots of content ... */}

      <TourStep id="step-5" intro="Far down the page">
        <Text>Near bottom</Text>
      </TourStep>
    </ScrollView>
  );
}

For directional scroll padding (useful with fixed headers/tab bars):

tour.start('tour', steps, {
  scrollPadding: {
    top: 80,     // Account for header
    bottom: 60,  // Account for tab bar
  },
});

Custom Storage Adapter

Replace AsyncStorage with your own persistence:

const customStorage = {
  getItem: async (key) => await myDB.get(key),
  setItem: async (key, value) => await myDB.set(key, value),
  removeItem: async (key) => await myDB.delete(key),
};

<IntroProvider storageAdapter={customStorage}>
  <App />
</IntroProvider>

Tours in Modals

When using tours with elements inside React Native modals, ensure the modal is rendered within the IntroProvider context. The tour overlay renders at the root level, so modal content must be measurable.

import { Modal } from 'react-native';
import { TourStep, useTour } from '@decaylabs/react-native-intro';

function MyScreen() {
  const tour = useTour();
  const [modalVisible, setModalVisible] = useState(false);

  const startModalTour = () => {
    // Ensure modal is open before starting tour
    setModalVisible(true);
    // Small delay to allow modal to render and layout
    setTimeout(() => {
      tour.start('modal-tour', [
        {
          id: 'step-1',
          targetId: 'modal-button',
          content: 'This button is inside a modal!',
        },
      ]);
    }, 100);
  };

  return (
    <View>
      <Button title="Open Modal Tour" onPress={startModalTour} />

      <Modal visible={modalVisible} onRequestClose={() => setModalVisible(false)}>
        <View style={styles.modalContent}>
          <TourStep id="modal-button">
            <Button title="Modal Action" onPress={() => {}} />
          </TourStep>
        </View>
      </Modal>
    </View>
  );
}

Important considerations for modals:

  • Add a small delay (50-100ms) after opening the modal before starting the tour to ensure layout is complete
  • The modal must be rendered within the IntroProvider tree
  • Use tour.stop() before closing the modal if a tour is active

Debug Logging

Enable detailed console logging to debug tour positioning and state issues:

import { setDebugEnabled } from '@decaylabs/react-native-intro';

// Enable debug logging
setDebugEnabled(true);

// Check Metro/Xcode console for logs:
// [TourOverlay] Step transition effect triggered {...}
// [Tooltip] Calculating position {...}
// [Positioning] calculateTooltipPosition called {...}

Debug logs include:

  • Element measurements and coordinates
  • Position calculation attempts and results
  • State transitions and animation events
  • Fallback positioning decisions

The example app includes a debug toggle in the Advanced screen.

Accessibility

The library provides comprehensive accessibility support:

  • VoiceOver/TalkBack: All interactive elements have proper labels
  • Screen reader announcements: Step changes are announced
  • Reduced motion: Respects system preference
  • Semantic roles: Proper button, dialog, and progressbar roles
// Check accessibility preferences
import { isReduceMotionEnabled, isScreenReaderEnabled } from '@decaylabs/react-native-intro';

const reduceMotion = await isReduceMotionEnabled();
const screenReader = await isScreenReaderEnabled();

Platform Support

| Platform | Version | |----------|---------| | React Native | 0.81.0+ | | React | 19.0.0+ | | iOS | 15.1+ | | Android | API 24+ | | Expo | 54.0.0+ (compatible, not required) |

TypeScript

Full TypeScript support with all types exported:

import type {
  // Core types
  TourOptions,
  HintOptions,
  StepConfig,
  HintConfig,

  // Theme types
  Theme,
  ThemeName,

  // Callback types
  TourCallbacks,
  HintCallbacks,

  // State types
  TourState,
  TourStateInfo,
  HintsState,

  // Control types
  TourControls,
  HintControls,

  // Hook return type
  UseIntroReturn,

  // Component props
  IntroProviderProps,
  TourStepProps,
  HintSpotProps,
} from '@decaylabs/react-native-intro';

Troubleshooting

Tooltip appears in wrong position

Ensure the target element is rendered before starting:

useEffect(() => {
  const timer = setTimeout(() => tour.start(), 100);
  return () => clearTimeout(timer);
}, []);

"Element not found" warning

The targetId in programmatic tours must match the id of a TourStep component. The library only knows about elements wrapped in TourStep:

// ✅ Correct - targetId matches TourStep id
<TourStep id="my-button">
  <Button title="Click" />
</TourStep>
tour.start('tour', [{ id: 'step-1', targetId: 'my-button', content: '...' }]);

// ❌ Wrong - no TourStep with this id
<Button id="my-button" title="Click" />  // Regular component, not registered!
tour.start('tour', [{ id: 'step-1', targetId: 'my-button', content: '...' }]);

Animations not smooth

  1. Ensure Reanimated is properly configured in babel.config.js
  2. Clear Metro cache: npx react-native start --reset-cache
  3. Rebuild the app

Tour doesn't start for repeat users

If using dontShowAgain: true, the tour is persisted when dismissed:

// Check if dismissed
if (tour.isDismissed('my-tour')) {
  // Show a "restart tour" button
}

// Clear dismissed state
tour.clearDismissed('my-tour');

Contributing

See DEVELOPER.md for development setup and MAINTAINER.md for release procedures.

License

MIT


Made with create-react-native-library