JSPM

rn-sherpa

0.1.0-beta.2
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q60553F
  • License MIT

A lightweight, flexible React Native library for creating powerful, step-by-step guided product tours with smart positioning and animations

Package Exports

  • rn-sherpa
  • rn-sherpa/src/index.tsx

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (rn-sherpa) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

rn-sherpa

A lightweight, flexible, and dependency-free library for creating powerful, step-by-step guided product tours for your React Native applications.

Inspired by the amazing Driver.js, this library lets you highlight UI components to guide your users, improve the onboarding process, and showcase new features in an elegant and simple way.

Features

  • Lightweight & Dependency-Free: Built from the ground up with only React and React Native
  • Declarative API: Define your tour steps in a simple array of objects
  • Fully Customizable: Full control over the look and feel of the popover and overlay
  • Smooth Animations: Support for fluid transitions between steps with Reanimated
  • Smart Positioning: Automatically adjusts popover position to stay visible on screen
  • Scroll-to-View: Automatically scrolls elements into view when tour steps change
  • Written in TypeScript: Type-safe with autocompletion for a better developer experience

Installation

npm install rn-sherpa react-native-reanimated
# or
yarn add rn-sherpa react-native-reanimated

Setup Reanimated

Add the Reanimated plugin to your babel.config.js:

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

Note: The Reanimated plugin must be listed last in the plugins array.

Quick Start

Here's a simple example to get you started:

import React from 'react';
import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import {
  TourProvider,
  TourOverlay,
  useTour,
  useStepRef,
  useAutoScroll,
  type TourConfig,
} from 'rn-sherpa';

function App() {
  const tourConfig: TourConfig = {
    steps: [
      {
        id: 'welcome',
        title: 'Welcome! 🎉',
        text: 'Let me show you around this app.',
        popoverPosition: 'center',
      },
      {
        id: 'header',
        title: 'App Header',
        text: 'This is your main navigation area.',
        popoverPosition: 'bottom',
        padding: 12,
        borderRadius: 12,
      },
      {
        id: 'button',
        title: 'Action Button',
        text: 'Tap this button to perform an action.',
        popoverPosition: 'bottom',
      },
    ],
    showButtons: true,
    showProgress: true,
    allowClose: true,
    animationDuration: 400,
    overlayColor: 'rgba(0, 0, 0, 0.75)',
  };

  return (
    <TourProvider config={tourConfig}>
      <SafeAreaProvider>
        <YourApp />
        <TourOverlay config={tourConfig} />
      </SafeAreaProvider>
    </TourProvider>
  );
}

function YourApp() {
  const tour = useTour();
  const scrollViewRef = React.useRef<ScrollView>(null);

  // Create refs for tour steps
  const headerRef = useStepRef<View>();
  const buttonRef = useStepRef<View>();

  // Automatically scroll to elements during tour
  useAutoScroll(scrollViewRef);

  // Assign refs to tour steps
  React.useEffect(() => {
    if (tour.currentStep) {
      switch (tour.currentStep.id) {
        case 'header':
          tour.currentStep.ref = headerRef;
          break;
        case 'button':
          tour.currentStep.ref = buttonRef;
          break;
      }
    }
  }, [tour.currentStep, headerRef, buttonRef]);

  return (
    <View style={{ flex: 1 }}>
      <ScrollView ref={scrollViewRef}>
        <View ref={headerRef} style={{ padding: 20, backgroundColor: '#007AFF' }}>
          <Text style={{ color: 'white', fontSize: 24 }}>My App</Text>
        </View>

        <TouchableOpacity
          onPress={tour.start}
          style={{ padding: 16, backgroundColor: '#28a745', margin: 20 }}
        >
          <Text style={{ color: 'white' }}>Start Tour</Text>
        </TouchableOpacity>

        <TouchableOpacity
          ref={buttonRef}
          style={{ padding: 16, backgroundColor: '#007AFF', margin: 20 }}
        >
          <Text style={{ color: 'white' }}>Important Button</Text>
        </TouchableOpacity>
      </ScrollView>
    </View>
  );
}

API Reference

TourProvider

Wrap your app with TourProvider to enable tour functionality.

<TourProvider config={tourConfig}>
  {/* Your app */}
</TourProvider>

TourConfig

Configuration object for the tour:

interface TourConfig {
  // Required
  steps: TourStep[];

  // UI Options
  showButtons?: boolean;          // Show navigation buttons (default: true)
  showProgress?: boolean;         // Show "X of Y" progress indicator (default: true)
  allowClose?: boolean;           // Allow closing tour with X button (default: true)

  // Animation
  animationDuration?: number;     // Animation duration in ms (default: 300)

  // Styling
  overlayColor?: string;          // Overlay color (default: 'rgba(0, 0, 0, 0.75)')
  popoverStyle?: ViewStyle;       // Custom styles for popover
  overlayStyle?: ViewStyle;       // Custom styles for overlay

  // Callbacks
  onStart?: () => void;           // Called when tour starts
  onComplete?: () => void;        // Called when tour completes normally
  onSkip?: () => void;            // Called when tour is closed/skipped
}

TourStep

Configuration for individual tour steps:

interface TourStep {
  id: string;
  text: string;
  title?: string;
  ref?: React.RefObject<any>;
  popoverContent?: ReactNode;
  popoverPosition?: 'top' | 'bottom' | 'left' | 'right' | 'center';
  onShow?: () => void;
  onHide?: () => void;
  padding?: number;
  borderRadius?: number;
}

useTour Hook

Access tour controls and state:

const {
  currentStepIndex,
  totalSteps,
  isActive,
  start,
  stop,
  next,
  previous,
  goToStep,
  currentStep,
} = useTour();

useStepRef Hook

Create refs for components to highlight:

const myComponentRef = useStepRef<View>();

<View ref={myComponentRef}>
  {/* Component to highlight */}
</View>

useAutoScroll Hook

Automatically scrolls to tour elements in ScrollView:

const scrollViewRef = useRef<ScrollView>(null);

// Basic usage
useAutoScroll(scrollViewRef);

// With options
useAutoScroll(scrollViewRef, {
  topPadding: 150,    // Padding from top (default: 100)
  animated: true,     // Animate scroll (default: true)
  enabled: true,      // Enable/disable (default: true)
});

<ScrollView ref={scrollViewRef}>
  {/* Your content */}
</ScrollView>

Key Features Explained

Smart Positioning

The library automatically adjusts popover positions to ensure they're always visible on screen. When you specify a popoverPosition, the library:

  1. Checks available space in all directions
  2. Automatically switches position if there's insufficient space
  3. Maintains proper spacing from screen edges

Example:

{
  id: 'bottom-button',
  title: 'Settings',
  text: 'Access your settings here.',
  popoverPosition: 'bottom', // Will auto-switch to 'top' if near bottom of screen
}

How it works:

  • If you choose bottom but there's less than 240px space below → switches to top
  • If you choose top but there's less than 240px space above → switches to bottom
  • Same logic applies for leftright
  • Animations automatically match the actual calculated position

Auto-Scroll to Elements

For scrollable content, you can implement auto-scrolling to ensure tour elements are visible:

function YourApp() {
  const tour = useTour();
  const scrollViewRef = React.useRef<ScrollView>(null);

  // Auto-scroll when tour step changes
  React.useEffect(() => {
    if (tour.isActive && tour.currentStep?.ref?.current && scrollViewRef.current) {
      const ref = tour.currentStep.ref.current;

      ref.measureLayout(
        scrollViewRef.current as any,
        (_x: number, y: number, _width: number, height: number) => {
          scrollViewRef.current?.scrollTo({
            y: Math.max(0, y - 100), // 100px padding from top
            animated: true,
          });
        },
        (error) => console.log('Measurement failed:', error)
      );
    }
  }, [tour.isActive, tour.currentStep, tour.currentStepIndex]);

  return (
    <ScrollView ref={scrollViewRef}>
      {/* Your content */}
    </ScrollView>
  );
}

Best Practices:

  • Always add padding when scrolling (e.g., y - 100) to avoid elements at screen edges
  • Use animated: true for smooth scrolling transitions
  • The library includes measurement retry logic to handle elements that need time to render

Spotlight Customization

Customize the spotlight cutout around highlighted elements:

{
  id: 'custom-spotlight',
  title: 'Custom Highlight',
  text: 'Notice the rounded corners and extra padding.',
  padding: 16,        // Extra space around element (default: 8)
  borderRadius: 20,   // Corner radius for spotlight (default: 8)
}

Advanced Usage

Custom Popover Content

const tourConfig: TourConfig = {
  steps: [
    {
      id: 'custom',
      title: 'Custom Content',
      text: 'This won\'t be shown',
      popoverContent: (
        <View>
          <Text>Your custom JSX content here!</Text>
        </View>
      ),
    },
  ],
};

Programmatic Control

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

  return (
    <View>
      <Button title="Start Tour" onPress={tour.start} />
      <Button title="Stop Tour" onPress={tour.stop} />
      <Button title="Next Step" onPress={tour.next} />
      <Button title="Previous Step" onPress={tour.previous} />
      <Button title="Go to Step 3" onPress={() => tour.goToStep(2)} />
    </View>
  );
}

Step Callbacks

const tourConfig: TourConfig = {
  steps: [
    {
      id: 'step1',
      title: 'Step 1',
      text: 'First step',
      onShow: () => console.log('Step 1 shown'),
      onHide: () => console.log('Step 1 hidden'),
    },
  ],
  onStart: () => console.log('Tour started'),
  onComplete: () => console.log('Tour completed'),
  onSkip: () => console.log('Tour skipped'),
};

Animation Customization

The library uses react-native-reanimated for smooth, performant animations:

const tourConfig: TourConfig = {
  steps: [...],
  animationDuration: 500, // Customize animation speed (default: 300ms)
};

Built-in Animations:

  • Overlay: Fade in/out with spotlight cutout animation
  • Popover: Position-aware slide animations
    • top position → Slides in from top
    • bottom position → Slides in from bottom
    • left position → Slides in from left
    • right position → Slides in from right
    • center position → Fades in
  • Scale effect: Subtle scale animation (0.9 → 1.0) on popovers

The animations automatically adapt to the actual calculated position, so if smart positioning switches from bottom to top, the animation will also switch to match.

Troubleshooting

Popover appears off-screen

Solution: The library includes smart positioning that automatically adjusts placement. If this still happens:

  1. Ensure elements have proper dimensions when measured
  2. Add a small delay before starting the tour to allow layout to complete:
    setTimeout(() => tour.start(), 100);

Element not highlighting correctly

Solution: Elements need to be measured before highlighting. The library includes automatic retry logic, but you can help by:

  1. Ensuring the element is rendered before the tour step activates
  2. Using onShow callback to verify the element is ready:
    {
      id: 'step1',
      title: 'Step 1',
      text: 'Content',
      onShow: () => console.log('Step showing - element should be rendered'),
    }

Scrollable content: Element not scrolling into view

Solution: Implement the auto-scroll pattern shown in the "Auto-Scroll to Elements" section above. The library handles measurement timing, but you need to provide the scroll logic.

Animation issues

Problem: Animations stuttering or not working

Solution:

  1. Ensure react-native-reanimated is properly installed
  2. Verify babel plugin is added and listed last in babel.config.js
  3. Rebuild your app after adding the Reanimated plugin
  4. For iOS: run pod install in the ios folder

TypeScript errors

Problem: Type errors with refs or config

Solution:

  1. Import types from the library:
    import type { TourConfig, TourStep } from 'rn-sherpa';
  2. Use generic type parameters with useStepRef:
    const myRef = useStepRef<View>();

Best Practices

1. Tour Step Organization

Structure your tour steps from top to bottom, following the natural reading order:

const tourConfig: TourConfig = {
  steps: [
    { id: 'welcome', popoverPosition: 'center' },    // Start with welcome
    { id: 'header', popoverPosition: 'bottom' },     // Then top elements
    { id: 'content', popoverPosition: 'top' },       // Middle elements
    { id: 'footer', popoverPosition: 'top' },        // Bottom elements
  ],
};

2. Position Selection

Choose positions that make sense for each element's location:

  • Top elements (header, navbar): Use popoverPosition: 'bottom'
  • Bottom elements (footer, tabs): Use popoverPosition: 'top'
  • Side elements: Use left or right based on screen position
  • Introductory/concluding steps: Use center for full-screen focus

The smart positioning system will auto-adjust if needed!

3. Implement Scroll-to-View

Always implement scroll-to-view for scrollable content to ensure great UX:

React.useEffect(() => {
  if (tour.isActive && tour.currentStep?.ref?.current && scrollViewRef.current) {
    // Scroll implementation here
  }
}, [tour.isActive, tour.currentStep, tour.currentStepIndex]);

4. Use Callbacks Wisely

Leverage callbacks for analytics, state management, or triggering actions:

const tourConfig: TourConfig = {
  steps: [
    {
      id: 'feature',
      title: 'New Feature',
      text: 'Check this out!',
      onShow: () => {
        // Track step view
        analytics.track('tour_step_viewed', { step: 'feature' });
      },
    },
  ],
  onComplete: () => {
    // Mark tour as completed
    AsyncStorage.setItem('tour_completed', 'true');
  },
};

5. Customize Spotlight for Context

Adjust padding and border radius based on the element type:

// Large padding for small buttons
{ id: 'small-icon', padding: 16, borderRadius: 20 }

// Minimal padding for large cards
{ id: 'card', padding: 8, borderRadius: 12 }

6. SafeAreaProvider Integration

Always wrap your app with SafeAreaProvider to handle notches and safe areas correctly:

import { SafeAreaProvider } from 'react-native-safe-area-context';

<TourProvider config={tourConfig}>
  <SafeAreaProvider>
    <YourApp />
  </SafeAreaProvider>
  <TourOverlay config={tourConfig} />
</TourProvider>

7. Test on Multiple Devices

The smart positioning system works across different screen sizes, but always test:

  • Small screens (iPhone SE)
  • Large screens (iPad, large Android phones)
  • Different orientations (if supported)

Examples

Check out the example app for a complete working demo.

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.