ExpoStartup

MultiStep

A component for creating guided, multi-step experiences like onboarding flows, wizards, and complex forms.

MultiStep

The MultiStep component provides a structured way to build guided, step-by-step user experiences. It's ideal for onboarding flows, wizards, and breaking down complex forms into manageable sections.

Features

  • Integrated Navigation: Built-in next, back, and skip functionality
  • Progress Indicator: Shows users where they are in the process
  • Animated Transitions: Smooth transitions between steps
  • Completion Handler: Execute code when the user completes all steps
  • Customizable Header: Optional header with back button and progress display
  • Cancel Option: Allow users to exit the flow at any point
  • Custom or Automatic Sizing: Automatically fits content or use custom dimensions

Import

import MultiStep, { Step } from '@/components/MultiStep';

Props

MultiStep Props

PropTypeDefaultDescription
childrenReactNodeStep components to be rendered within the multi-step flow
onComplete() => voidFunction called when all steps are completed
onClose() => voidFunction called when the flow is closed or canceled
showHeaderbooleantrueWhether to show the header with back button and progress indicator
hideBackOnFirstStepbooleanfalseWhether to hide the back button on the first step
headerTitlestringTitle text to display in the header
backTextstring"Back"Text for the back button
nextTextstring"Continue"Text for the next/continue button
skipTextstring"Skip"Text for the skip option
showSkipbooleanfalseWhether to show a skip option
onSkip() => voidFunction called when the skip option is selected
heightnumberCustom height for the component
widthnumberCustom width for the component
styleStyleProp<ViewStyle>Additional styles for the container

Step Props

PropTypeDefaultDescription
childrenReactNodeContent to display within this step
hasNextButtonbooleantrueWhether to show the next/continue button
nextLabelstringCustom text for the next button (overrides parent's nextText)
nextDisabledbooleanfalseWhether the next button should be disabled
onNext() => void | Promise<void>Custom handler for next button (can be async)

Usage Examples

Basic Usage

import React from 'react';
import { View, Text } from 'react-native';
import MultiStep, { Step } from '../components/MultiStep';
 
function OnboardingFlow() {
  const handleComplete = () => {
    console.log('Onboarding completed!');
    // Navigate to main app or set onboarding completed flag
  };
  
  const handleClose = () => {
    console.log('Onboarding canceled');
    // Handle premature exit
  };
  
  return (
    <View className="flex-1 justify-center items-center">
      <MultiStep 
        onComplete={handleComplete}
        onClose={handleClose}
        headerTitle="Get Started"
      >
        <Step>
          <View className="p-6 items-center">
            <Text className="text-2xl font-bold mb-4">Welcome!</Text>
            <Text className="text-center mb-8">
              We're excited to have you join us. Let's get you set up in just a few steps.
            </Text>
          </View>
        </Step>
        
        <Step>
          <View className="p-6 items-center">
            <Text className="text-2xl font-bold mb-4">Personalize</Text>
            <Text className="text-center mb-8">
              Tell us a bit about yourself so we can customize your experience.
            </Text>
            {/* Form fields would go here */}
          </View>
        </Step>
        
        <Step>
          <View className="p-6 items-center">
            <Text className="text-2xl font-bold mb-4">All Set!</Text>
            <Text className="text-center mb-8">
              You're ready to start using the app. Tap continue to begin.
            </Text>
          </View>
        </Step>
      </MultiStep>
    </View>
  );
}

Custom Step Navigation

import React, { useState } from 'react';
import { View, Text, TextInput } from 'react-native';
import MultiStep, { Step } from '../components/MultiStep';
 
function RegistrationFlow() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    password: ''
  });
  const [errors, setErrors] = useState({});
  
  const validateEmail = () => {
    if (!formData.email || !formData.email.includes('@')) {
      setErrors({...errors, email: 'Please enter a valid email'});
      return false;
    }
    return true;
  };
  
  const handleComplete = () => {
    // Submit registration data
    console.log('Registration data:', formData);
  };
  
  return (
    <View className="flex-1 justify-center items-center p-4">
      <MultiStep 
        onComplete={handleComplete}
        headerTitle="Create Account"
        style={{ width: '100%', maxWidth: 500 }}
      >
        <Step 
          nextLabel="Continue" 
          nextDisabled={!formData.name}
        >
          <View className="p-4 w-full">
            <Text className="text-xl font-bold mb-6">Personal Information</Text>
            
            <Text className="mb-2 font-medium">Full Name</Text>
            <TextInput
              className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-4"
              placeholder="Enter your full name"
              value={formData.name}
              onChangeText={(text) => setFormData({...formData, name: text})}
            />
          </View>
        </Step>
        
        <Step 
          nextLabel="Continue"
          nextDisabled={!formData.email}
          onNext={validateEmail} // Will only proceed if validation returns true
        >
          <View className="p-4 w-full">
            <Text className="text-xl font-bold mb-6">Contact Information</Text>
            
            <Text className="mb-2 font-medium">Email Address</Text>
            <TextInput
              className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-1"
              placeholder="Enter your email"
              value={formData.email}
              onChangeText={(text) => {
                setFormData({...formData, email: text});
                if (errors.email) setErrors({...errors, email: null});
              }}
              keyboardType="email-address"
              autoCapitalize="none"
            />
            {errors.email && (
              <Text className="text-red-500 mb-4">{errors.email}</Text>
            )}
          </View>
        </Step>
        
        <Step nextLabel="Create Account">
          <View className="p-4 w-full">
            <Text className="text-xl font-bold mb-6">Security</Text>
            
            <Text className="mb-2 font-medium">Password</Text>
            <TextInput
              className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-4"
              placeholder="Create a password"
              value={formData.password}
              onChangeText={(text) => setFormData({...formData, password: text})}
              secureTextEntry={true}
            />
            
            <Text className="text-gray-500 text-sm">
              By creating an account, you agree to our Terms of Service and Privacy Policy.
            </Text>
          </View>
        </Step>
      </MultiStep>
    </View>
  );
}

Skippable Steps

import React from 'react';
import { View, Text } from 'react-native';
import MultiStep, { Step } from '../components/MultiStep';
 
function OnboardingWithOptionalSteps() {
  const handleSkip = () => {
    console.log('User skipped optional steps');
    // Maybe set some default values
  };
  
  return (
    <View className="flex-1 justify-center items-center">
      <MultiStep 
        onComplete={() => console.log('Completed')}
        showSkip={true}
        skipText="Skip Tutorial"
        onSkip={handleSkip}
      >
        <Step>
          <View className="p-6 items-center">
            <Text className="text-2xl font-bold mb-4">Essential Setup</Text>
            <Text className="text-center mb-8">
              This step is required to get started with the app.
            </Text>
          </View>
        </Step>
        
        <Step>
          <View className="p-6 items-center">
            <Text className="text-2xl font-bold mb-4">Advanced Features</Text>
            <Text className="text-center mb-8">
              Learn about some powerful features you might want to use later.
            </Text>
            <Text className="text-gray-500 italic">
              This step is optional. You can skip the tutorial if you want.
            </Text>
          </View>
        </Step>
        
        <Step>
          <View className="p-6 items-center">
            <Text className="text-2xl font-bold mb-4">Tips & Tricks</Text>
            <Text className="text-center mb-8">
              Discover shortcuts and optimizations to work faster.
            </Text>
          </View>
        </Step>
      </MultiStep>
    </View>
  );
}

With Form Validation

import React, { useState } from 'react';
import { View, Text, TextInput } from 'react-native';
import MultiStep, { Step } from '../components/MultiStep';
 
function PaymentFlow() {
  const [cardData, setCardData] = useState({
    number: '',
    name: '',
    expiry: '',
    cvv: ''
  });
  
  const [errors, setErrors] = useState({});
  
  const validateCardNumber = () => {
    // Simple validation for example purposes
    if (cardData.number.replace(/\s/g, '').length !== 16) {
      setErrors({...errors, number: 'Card number must be 16 digits'});
      return false;
    }
    return true;
  };
  
  const validateExpiry = () => {
    const regex = /^(0[1-9]|1[0-2])\/\d{2}$/;
    if (!regex.test(cardData.expiry)) {
      setErrors({...errors, expiry: 'Use format MM/YY'});
      return false;
    }
    return true;
  };
  
  const validateCVV = async () => {
    // Simulating an async validation
    return new Promise((resolve) => {
      setTimeout(() => {
        if (cardData.cvv.length < 3) {
          setErrors({...errors, cvv: 'CVV must be 3 or 4 digits'});
          resolve(false);
        } else {
          resolve(true);
        }
      }, 500);
    });
  };
  
  const processPayment = () => {
    console.log('Processing payment with:', cardData);
    // Integration with payment processor would go here
  };
  
  return (
    <View className="flex-1 justify-center items-center p-4">
      <MultiStep 
        onComplete={processPayment}
        headerTitle="Payment Details"
      >
        <Step onNext={validateCardNumber}>
          <View className="p-4 w-full">
            <Text className="text-xl font-bold mb-6">Card Information</Text>
            
            <Text className="mb-2 font-medium">Card Number</Text>
            <TextInput
              className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-1"
              placeholder="1234 5678 9012 3456"
              value={cardData.number}
              onChangeText={(text) => {
                setCardData({...cardData, number: text});
                if (errors.number) setErrors({...errors, number: null});
              }}
              keyboardType="number-pad"
            />
            {errors.number && (
              <Text className="text-red-500 mb-4">{errors.number}</Text>
            )}
            
            <Text className="mb-2 font-medium mt-4">Cardholder Name</Text>
            <TextInput
              className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-4"
              placeholder="Name as it appears on card"
              value={cardData.name}
              onChangeText={(text) => setCardData({...cardData, name: text})}
            />
          </View>
        </Step>
        
        <Step onNext={validateExpiry}>
          <View className="p-4 w-full">
            <Text className="text-xl font-bold mb-6">Expiration Date</Text>
            
            <Text className="mb-2 font-medium">Expiry (MM/YY)</Text>
            <TextInput
              className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-1"
              placeholder="MM/YY"
              value={cardData.expiry}
              onChangeText={(text) => {
                setCardData({...cardData, expiry: text});
                if (errors.expiry) setErrors({...errors, expiry: null});
              }}
            />
            {errors.expiry && (
              <Text className="text-red-500 mb-4">{errors.expiry}</Text>
            )}
          </View>
        </Step>
        
        <Step onNext={validateCVV} nextLabel="Submit Payment">
          <View className="p-4 w-full">
            <Text className="text-xl font-bold mb-6">Security Code</Text>
            
            <Text className="mb-2 font-medium">CVV/CVC</Text>
            <TextInput
              className="border border-gray-300 dark:border-gray-700 rounded-lg p-3 mb-1"
              placeholder="3 or 4 digit code"
              value={cardData.cvv}
              onChangeText={(text) => {
                setCardData({...cardData, cvv: text});
                if (errors.cvv) setErrors({...errors, cvv: null});
              }}
              keyboardType="number-pad"
              maxLength={4}
              secureTextEntry={true}
            />
            {errors.cvv && (
              <Text className="text-red-500 mb-4">{errors.cvv}</Text>
            )}
            
            <Text className="text-gray-500 mt-4">
              Your payment information is securely processed. We do not store your full card details.
            </Text>
          </View>
        </Step>
      </MultiStep>
    </View>
  );
}

Integration Examples

Onboarding Flow

import React from 'react';
import { View, Text, Image } from 'react-native';
import MultiStep, { Step } from '../components/MultiStep';
import AsyncStorage from '@react-native-async-storage/async-storage';
 
function AppOnboarding({ navigation }) {
  const completeOnboarding = async () => {
    try {
      // Set flag that onboarding is complete
      await AsyncStorage.setItem('hasCompletedOnboarding', 'true');
      // Navigate to main app
      navigation.replace('MainApp');
    } catch (error) {
      console.error('Error saving onboarding state:', error);
    }
  };
  
  return (
    <View className="flex-1">
      <MultiStep 
        onComplete={completeOnboarding}
        onClose={() => navigation.replace('MainApp')}
        showSkip={true}
        skipText="Skip"
        onSkip={completeOnboarding}
      >
        <Step>
          <View className="p-6 items-center">
            <Image 
              source={require('../assets/welcome-illustration.png')}
              className="w-64 h-64 mb-8"
              resizeMode="contain"
            />
            <Text className="text-2xl font-bold mb-4">Welcome to AppName</Text>
            <Text className="text-center mb-8">
              The all-in-one solution for managing your daily tasks and projects.
            </Text>
          </View>
        </Step>
        
        <Step>
          <View className="p-6 items-center">
            <Image 
              source={require('../assets/tasks-illustration.png')}
              className="w-64 h-64 mb-8"
              resizeMode="contain"
            />
            <Text className="text-2xl font-bold mb-4">Manage Tasks</Text>
            <Text className="text-center mb-8">
              Create, organize, and complete tasks with ease. Set priorities and deadlines to stay on track.
            </Text>
          </View>
        </Step>
        
        <Step>
          <View className="p-6 items-center">
            <Image 
              source={require('../assets/collaboration-illustration.png')}
              className="w-64 h-64 mb-8"
              resizeMode="contain"
            />
            <Text className="text-2xl font-bold mb-4">Collaborate</Text>
            <Text className="text-center mb-8">
              Share projects with teammates, assign tasks, and track progress together.
            </Text>
          </View>
        </Step>
        
        <Step>
          <View className="p-6 items-center">
            <Image 
              source={require('../assets/getstarted-illustration.png')}
              className="w-64 h-64 mb-8"
              resizeMode="contain"
            />
            <Text className="text-2xl font-bold mb-4">Ready to Start?</Text>
            <Text className="text-center mb-8">
              Your productivity journey begins now. Let's create your first project!
            </Text>
          </View>
        </Step>
      </MultiStep>
    </View>
  );
}

Feature Tutorial

import React from 'react';
import { View, Text, Image } from 'react-native';
import MultiStep, { Step } from '../components/MultiStep';
 
function FeatureTutorial({ onDismiss, featureName }) {
  return (
    <View className="absolute inset-0 bg-black/50 justify-center items-center z-50">
      <View className="w-11/12 max-w-md">
        <MultiStep 
          onComplete={onDismiss}
          onClose={onDismiss}
          headerTitle={`${featureName} Tutorial`}
          style={{ backgroundColor: 'white', borderRadius: 12 }}
        >
          <Step>
            <View className="p-6 items-center">
              <Image 
                source={require('../assets/feature-step1.png')}
                className="w-full h-40 mb-4"
                resizeMode="contain"
              />
              <Text className="text-xl font-bold mb-2">Getting Started</Text>
              <Text className="text-center">
                Tap the '+' button in the bottom right corner to create a new item.
              </Text>
            </View>
          </Step>
          
          <Step>
            <View className="p-6 items-center">
              <Image 
                source={require('../assets/feature-step2.png')}
                className="w-full h-40 mb-4"
                resizeMode="contain"
              />
              <Text className="text-xl font-bold mb-2">Configure Options</Text>
              <Text className="text-center">
                Set your preferences and customize settings to your needs.
              </Text>
            </View>
          </Step>
          
          <Step>
            <View className="p-6 items-center">
              <Image 
                source={require('../assets/feature-step3.png')}
                className="w-full h-40 mb-4"
                resizeMode="contain"
              />
              <Text className="text-xl font-bold mb-2">Complete Setup</Text>
              <Text className="text-center">
                Review your settings and tap 'Save' to finish.
              </Text>
            </View>
          </Step>
        </MultiStep>
      </View>
    </View>
  );
}

Best Practices

Step Complexity

  • Keep Steps Focused: Each step should focus on a single task or piece of information
  • Logical Progression: Order steps in a logical sequence from simple to complex
  • Balance Step Count: Too many steps can frustrate users, while too few might overwhelm them with information

Validation

  • Validate at the Step Level: Use the onNext prop to validate data before proceeding
  • Clear Error Messages: When validation fails, provide clear guidance on how to fix issues
  • Prevent Data Loss: Don't clear form data when a user navigates backward

User Experience

  • Show Progress: Always indicate to users where they are in the process
  • Allow Exit: Provide a way for users to exit the flow, especially for optional processes
  • Save Progress: For longer flows, consider saving progress to allow users to continue later

Accessibility

  • Screen Reader Support: Ensure the component and its children are accessible to screen readers
  • Keyboard Navigation: Support keyboard navigation for desktop environments
  • Sufficient Contrast: Maintain readable contrast for progress indicators and navigation elements

Implementation Details

The MultiStep component uses React's state management to track the current step index and handle navigation between steps. It accepts Step components as children, which are rendered based on the current step index.

The component employs React Native's Animated API to create smooth transitions between steps, sliding content horizontally as users navigate through the flow.

The progress indicator at the top of the component calculates its width based on the current step index and total number of steps, providing users with a visual representation of their progress.

For navigation, the component offers built-in "next" and "back" functionality, with customizable button text. Custom navigation logic can be implemented through the onNext prop of each Step component.

Notes

  • When using custom dimensions with height and width props, ensure there's enough space for content
  • For form validation, remember that onNext can return a Promise for asynchronous validation
  • If you need to access the current step index, pass a ref to the MultiStep component and use the exposed methods
  • The component automatically fits content height by default unless a fixed height is specified

On this page