A deep-dive reference for interviews and production projects. Covers installation, all APIs, real bugs encountered, fixes, and best practices.
- What is react-native-copilot?
- Installation
- Core Architecture & How It Works
- CopilotProvider — The Root Setup
- CopilotStep — Marking Elements
- walkthroughable — Making Components Highlightable
- useCopilot Hook — Full API
- Overlays: view vs svg
- Custom Tooltip Component
- Custom Step Number Component
- Navigation Functions Explained
- Event System (copilotEvents)
- Styling & Customization
- Custom SVG Mask Path
- ScrollView Support
- i18n / Custom Labels
- Real Bugs & Fixes (From This Project)
- Complete Working Example
- Interview Q&A Cheat Sheet
react-native-copilot is a React Native library that lets you build step-by-step onboarding tours — the kind where a dark overlay appears and a tooltip highlights specific UI elements one at a time.
Real-world use cases:
- First-time user onboarding
- Feature discovery after an update
- Contextual help / guided walkthroughs
- Accessibility-friendly tutorials
# Install the main package
yarn add react-native-copilot
# or
npm install --save react-native-copilot
# Install SVG support (recommended for smooth animation)
expo install react-native-svg
# or for bare React Native:
yarn add react-native-svg
cd ios && pod installPackage versions used in this project:
{
"react-native-copilot": "^3.3.3",
"react-native-svg": "15.12.1"
}Understanding the internals helps you debug and customize effectively.
CopilotProvider (Context)
│
├── Registers all CopilotSteps (by order number)
├── Manages current step state
├── Controls overlay rendering
└── Exposes useCopilot() hook to all children
CopilotStep (wrapper)
│
├── Registers itself with Provider on mount
├── Measures its own position on screen (onLayout)
└── Passes { ref, onLayout } as `copilot` prop to child
walkthroughable(Component)
│
└── HOC that spreads the `copilot` prop onto the root element
so the library can measure where to draw the overlay mask
useCopilot()
│
└── Returns: start, stop, goToNext, goToPrev, goToNth,
currentStep, isFirstStep, isLastStep, copilotEvents
Flow when start() is called:
- Library sorts all registered
CopilotSteps byorder - Measures position of step 1's element on screen
- Renders the overlay with a "hole" cut out around that element
- Renders the tooltip near the hole
- User taps Next → moves to step 2, re-measures, re-renders overlay
Wrap your entire app (or the relevant screen tree) with CopilotProvider.
// _layout.tsx (Expo Router) or App.tsx
import { CopilotProvider } from "react-native-copilot";
export default function RootLayout() {
return (
<CopilotProvider
overlay="svg" // "svg" | "view"
verticalOffset={36} // Compensates for status bar height
backdropColor="rgba(0,0,0,0.6)"
arrowColor="#007AFF"
tooltipStyle={{ borderRadius: 12 }}
labels={{
previous: "Back",
next: "Next",
skip: "Skip",
finish: "Done",
}}
>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
</Stack>
</CopilotProvider>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
overlay |
"svg" | "view" |
auto-detected | Which overlay renderer to use |
verticalOffset |
number |
0 |
Shifts tooltip position vertically — use 36 to fix status bar misalignment |
backdropColor |
string |
"rgba(0,0,0,0.4)" |
The dark overlay color |
arrowColor |
string |
"#fff" |
Tooltip arrow color |
tooltipStyle |
StyleProp<ViewStyle> |
{} |
Extra styles on the tooltip wrapper |
tooltipComponent |
React.ComponentType |
built-in | Your custom tooltip component |
stepNumberComponent |
React.ComponentType |
built-in | Your custom step number badge |
labels |
object |
English strings | Custom button labels |
svgMaskPath |
function |
rectangular | Custom SVG cutout shape |
animationDuration |
number |
300 |
Overlay animation speed (ms) |
stopOnOutsideClick |
boolean |
false |
Stop tour when user taps outside tooltip |
active |
boolean |
true |
Whether the provider is active |
CopilotStep is a wrapper that registers a UI element as a tour step.
import { CopilotStep } from "react-native-copilot";
<CopilotStep
text="This button submits your form."
order={1}
name="submit-button"
active={true} // optional — set false to skip this step
>
<WalkthroughableTouchableOpacity style={styles.button}>
<Text>Submit</Text>
</WalkthroughableTouchableOpacity>
</CopilotStep>| Prop | Type | Required | Description |
|---|---|---|---|
name |
string |
✅ | Unique identifier for this step |
order |
number |
✅ | Position in the tour sequence (1-indexed) |
text |
string |
✅ | Description shown in the tooltip |
active |
boolean |
❌ | false skips this step entirely |
Important rules:
ordervalues must be unique across all steps- Steps are always sorted by
order, not by render order in JSX - All
CopilotStepcomponents must be mounted before callingstart()
walkthroughable is a Higher-Order Component (HOC) that makes any React Native component work with the copilot overlay system.
import { walkthroughable } from "react-native-copilot";
import { View, Text, TouchableOpacity, Image } from "react-native";
// Create walkthroughable versions of any built-in component
const WalkthroughableView = walkthroughable(View);
const WalkthroughableText = walkthroughable(Text);
const WalkthroughableTouchableOpacity = walkthroughable(TouchableOpacity);
const WalkthroughableImage = walkthroughable(Image);
// Usage — exactly like the original component
<CopilotStep text="..." order={1} name="...">
<WalkthroughableView style={styles.card}>
<Text>Content inside</Text>
</WalkthroughableView>
</CopilotStep>If you have your own custom component, spread the copilot prop onto its outermost element:
import { CopilotStep } from "react-native-copilot";
// Your custom component must accept and spread the copilot prop
const MyCard = ({ copilot, children }) => (
<View {...copilot} style={styles.card}> {/* <-- spread here */}
{children}
</View>
);
// Usage
<CopilotStep text="This is a card" order={1} name="card">
<MyCard>
<Text>Hello</Text>
</MyCard>
</CopilotStep>The copilot prop contains { ref, onLayout } — both are needed for position measurement.
import { useCopilot } from "react-native-copilot";
const MyComponent = () => {
const {
start, // () => void — starts the tour from step 1
stop, // () => void — stops/ends the tour
goToNext, // () => void — moves to next step
goToPrev, // () => void — moves to previous step
goToNth, // (n: number) => void — jump to step N (1-indexed)
currentStep, // Step | undefined — the currently active step object
isFirstStep, // boolean
isLastStep, // boolean
copilotEvents, // EventEmitter — for start/stop/stepChange events
} = useCopilot();
};// Basic — starts from step 1
start();
// With a ScrollView ref — library will auto-scroll to each step
start(undefined, scrollViewRef);
// From a specific step name
start("search"); // starts from the step named "search"Jumps to any step by its order number (1-indexed):
// Jump to step 3
goToNth(3);
// Useful for "pausing" the tour for user input:
goToNth(5); // skip to step 5 after user completes an action<CopilotProvider overlay="svg">- Uses an SVG
<Path>to draw the cutout - Smooth animation as mask moves between steps
- Requires
react-native-svg - Best for iOS and Android
<CopilotProvider overlay="view">- Uses 4
<View>rectangles around the target element - No animation (or sluggish on some Android devices)
- No extra dependencies
- Use only when SVG isn't available
Auto-detection: If you don't specify overlay, the library checks if react-native-svg is installed and uses SVG if available, otherwise falls back to view.
Replace the built-in tooltip with your own branded design:
import { useCopilot } from "react-native-copilot";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
const CustomTooltip = () => {
const {
isFirstStep,
isLastStep,
goToNext,
goToPrev,
goToNth,
stop,
currentStep,
} = useCopilot();
return (
<View style={styles.tooltip}>
{/* Step description */}
<Text style={styles.tooltipText}>{currentStep?.text}</Text>
{/* Navigation row */}
<View style={styles.buttonRow}>
<TouchableOpacity onPress={stop}>
<Text style={styles.skipText}>Skip</Text>
</TouchableOpacity>
<View style={styles.navButtons}>
{!isFirstStep && (
<TouchableOpacity onPress={goToPrev} style={styles.btn}>
<Text>← Back</Text>
</TouchableOpacity>
)}
<TouchableOpacity
onPress={isLastStep ? stop : goToNext}
style={[styles.btn, styles.primaryBtn]}
>
<Text style={styles.primaryText}>
{isLastStep ? "Finish ✓" : "Next →"}
</Text>
</TouchableOpacity>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
tooltip: {
backgroundColor: "#1C1C1E",
borderRadius: 14,
padding: 16,
maxWidth: 300,
},
tooltipText: { color: "#fff", fontSize: 15, marginBottom: 12 },
buttonRow: { flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
skipText: { color: "#8E8E93", fontSize: 13 },
navButtons: { flexDirection: "row", gap: 8 },
btn: { paddingHorizontal: 14, paddingVertical: 8, borderRadius: 8, backgroundColor: "#2C2C2E" },
primaryBtn: { backgroundColor: "#007AFF" },
primaryText: { color: "#fff", fontWeight: "600" },
});
// Register it
<CopilotProvider tooltipComponent={CustomTooltip}>
<App />
</CopilotProvider>import { useCopilot } from "react-native-copilot";
const StepNumber = () => {
const { currentStep } = useCopilot();
return (
<View style={{
width: 30, height: 30, borderRadius: 15,
backgroundColor: "#007AFF", justifyContent: "center", alignItems: "center"
}}>
<Text style={{ color: "#fff", fontWeight: "700" }}>
{currentStep?.order}
</Text>
</View>
);
};
<CopilotProvider stepNumberComponent={StepNumber}>
<App />
</CopilotProvider>| Function | What it does | Notes |
|---|---|---|
start() |
Begin tour from step 1 | All steps must be mounted first |
stop() |
End the tour immediately | Fires the stop event |
goToNext() |
Advance one step | Does nothing if on last step |
goToPrev() |
Go back one step | Does nothing if on first step |
goToNth(n) |
Jump to step n |
n matches order prop, 1-indexed |
const { goToNth, stop } = useCopilot();
// Tour is on step 3 (a form field)
// User fills in the form, then you resume:
const handleFormComplete = () => {
goToNth(4); // jump to step 4 to continue the tour
};The library uses mitt (a tiny event emitter) under the hood.
import { useCopilot } from "react-native-copilot";
import { useEffect } from "react";
const HomeScreen = () => {
const { copilotEvents } = useCopilot();
useEffect(() => {
// Tour started
const onStart = () => console.log("Tour started");
// Tour ended or skipped
const onStop = () => {
console.log("Tour finished");
// e.g. save to AsyncStorage that user completed onboarding
};
// Step changed — receives the new Step object
const onStepChange = (step) => {
console.log("Now on step:", step.name, step.order);
};
copilotEvents.on("start", onStart);
copilotEvents.on("stop", onStop);
copilotEvents.on("stepChange", onStepChange);
// Always clean up listeners
return () => {
copilotEvents.off("start", onStart);
copilotEvents.off("stop", onStop);
copilotEvents.off("stepChange", onStepChange);
};
}, []);
};| Event | When it fires | Argument |
|---|---|---|
start |
Tour begins | none |
stop |
Tour ends or is skipped | none |
stepChange |
Moving to a new step | Step object |
type Step = {
name: string; // The name prop you gave CopilotStep
order: number; // The order prop
text: string; // The text prop
target: any; // ref to the measured element
wrapper: any; // ref to the CopilotStep wrapper
};<CopilotProvider
tooltipStyle={{
backgroundColor: "#1C1C1E",
borderRadius: 14,
paddingTop: 8,
// Fixed width (override dynamic calculation):
width: Dimensions.get("window").width - 32,
maxWidth: Dimensions.get("window").width - 32,
left: 16,
}}
><CopilotProvider arrowColor="#007AFF"><CopilotProvider backdropColor="rgba(0, 0, 50, 0.85)">Critical for apps with status bars or custom headers:
<CopilotProvider verticalOffset={36}>
{/* 36 works well for most iOS/Android status bar heights */}By default the mask cuts a rectangle around the target. You can change it to any shape.
const circleMaskPath = ({ position, canvasSize }) =>
`M0,0H${canvasSize.x}V${canvasSize.y}H0V0Z` +
`M${position.x._value},${position.y._value}` +
`Za50 50 0 1 0 100 0 50 50 0 1 0-100 0`;
<CopilotProvider svgMaskPath={circleMaskPath}>const customPath = ({ position, size, canvasSize, step }) => {
if (step?.name === "avatar") {
// Circle for the avatar step
return `M0,0H${canvasSize.x}V${canvasSize.y}H0V0Z` +
`M${position.x._value},${position.y._value}Za50 50 0 1 0 100 0 50 50 0 1 0-100 0`;
}
// Default rectangle for all other steps
return `M0,0H${canvasSize.x}V${canvasSize.y}H0V0Z` +
`M${position.x._value},${position.y._value}` +
`H${position.x._value + size.x._value}` +
`V${position.y._value + size.y._value}` +
`H${position.x._value}V${position.y._value}Z`;
};
<CopilotProvider svgMaskPath={customPath}>type SvgMaskPathFn = (args: {
size: Animated.ValueXY; // width/height of the highlighted element
position: Animated.ValueXY; // x/y position on screen
canvasSize: { x: number; y: number }; // full screen dimensions
step: Step; // the current step object
}) => string; // must return a valid SVG path stringWhen your tour steps span a scrollable list, pass the ScrollView ref to start():
import { useRef } from "react";
import { ScrollView } from "react-native";
import { useCopilot } from "react-native-copilot";
const HomeScreen = () => {
const { start } = useCopilot();
const scrollRef = useRef(null);
useEffect(() => {
const timer = setTimeout(() => {
start(undefined, scrollRef); // pass ref as second argument
}, 1000);
return () => clearTimeout(timer);
}, []);
return (
<ScrollView ref={scrollRef}>
{/* CopilotSteps here */}
</ScrollView>
);
};The library will automatically scroll to bring each highlighted element into view.
<CopilotProvider
labels={{
previous: "Zurück", // German
next: "Weiter",
skip: "Überspringen",
finish: "Beenden",
}}
>
// Or Hindi:
labels={{
previous: "पिछला",
next: "अगला",
skip: "छोड़ें",
finish: "समाप्त",
}}These are real bugs encountered during development of this demo, documented so you never hit them again.
Symptom: Blank white screen, no errors.
Cause: JavaScript's Automatic Semicolon Insertion (ASI). When return is on its own line, JS treats it as return undefined and the JSX below it is dead code.
// ❌ BROKEN — returns undefined, JSX never executes
export default function RootLayout() {
return;
<CopilotProvider overlay="svg">
<Stack />
</CopilotProvider>;
}
// ✅ FIXED — parentheses attach the JSX to the return
export default function RootLayout() {
return (
<CopilotProvider overlay="svg">
<Stack />
</CopilotProvider>
);
}Rule: Any time JSX starts on a new line after return, always wrap in ().
Symptom: Tour starts, then immediately restarts in a loop (you can see the tooltip flickering).
Cause: start from useCopilot() is a new function reference on every render. Putting it in useEffect's dependency array tells React "re-run this effect when start changes" — which is every render.
// ❌ BROKEN — infinite loop
useEffect(() => {
const timer = setTimeout(() => start(), 600);
return () => clearTimeout(timer);
}, [start]); // <-- start changes every render
// ✅ FIXED — empty deps, runs once on mount
useEffect(() => {
const timer = setTimeout(() => start(), 1000);
return () => clearTimeout(timer);
}, []); // eslint-disable-line react-hooks/exhaustive-depsSymptom: After the loop fix with useRef, the tour still doesn't start in development.
Cause: React Strict Mode (enabled by default in Expo/development) intentionally double-mounts components to help detect side effects. The sequence is:
1st mount → hasStarted.current = false → set true, schedule timer
unmount → cleanup fires, timer CLEARED
2nd mount → hasStarted.current = true → skips entirely ❌
The useRef persists across the remount so the guard is already true on the second mount.
// ❌ BROKEN in development (Strict Mode)
const hasStarted = useRef(false);
useEffect(() => {
if (hasStarted.current) return;
hasStarted.current = true;
const timer = setTimeout(() => start(), 600);
return () => clearTimeout(timer);
}, []);
// ✅ FIXED — let cleanup handle it, use longer delay for safety
useEffect(() => {
const timer = setTimeout(() => start(), 1000);
return () => clearTimeout(timer);
}, []);
// Strict Mode: 1st timer clears, 2nd mount starts a fresh timer ✓
// Production: runs once normally ✓Symptom: The overlay highlight is correct but the tooltip box floats at the wrong Y position on screen.
Fix: Add verticalOffset to CopilotProvider to compensate for the status bar height:
<CopilotProvider overlay="svg" verticalOffset={36}>import { Stack } from "expo-router";
import { CopilotProvider } from "react-native-copilot";
export default function RootLayout() {
return (
<CopilotProvider overlay="svg" verticalOffset={36}>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
</Stack>
</CopilotProvider>
);
}import React, { useEffect } from "react";
import {
StyleSheet, Text, View, TouchableOpacity,
SafeAreaView, ScrollView, StatusBar,
} from "react-native";
import { CopilotStep, walkthroughable, useCopilot } from "react-native-copilot";
const WalkthroughableView = walkthroughable(View);
const WalkthroughableTouchableOpacity = walkthroughable(TouchableOpacity);
export default function HomeScreen() {
const { start } = useCopilot();
useEffect(() => {
// Empty deps [] = runs once. 1000ms delay ensures all CopilotSteps are mounted.
const timer = setTimeout(() => start(), 1000);
return () => clearTimeout(timer);
}, []);
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<ScrollView contentContainerStyle={styles.scroll} showsVerticalScrollIndicator={false}>
{/* STEP 1 — Header */}
<CopilotStep text="Your personalized greeting and profile avatar." order={1} name="header">
<WalkthroughableView style={styles.header}>
<View>
<Text style={styles.greeting}>Good morning 👋</Text>
<Text style={styles.username}>Alex Johnson</Text>
</View>
<View style={styles.avatarContainer}>
<Text style={styles.avatarText}>AJ</Text>
<View style={styles.notificationDot} />
</View>
</WalkthroughableView>
</CopilotStep>
{/* STEP 2 — Search */}
<CopilotStep text="Search for anything inside the app." order={2} name="search">
<WalkthroughableTouchableOpacity style={styles.searchBar}>
<Text style={styles.searchIcon}>🔍</Text>
<Text style={styles.searchPlaceholder}>Search anything...</Text>
</WalkthroughableTouchableOpacity>
</CopilotStep>
{/* STEP 3 — Stats */}
<CopilotStep text="Your key metrics at a glance." order={3} name="stats">
<WalkthroughableView style={styles.statsRow}>
<View style={[styles.statCard, { borderLeftColor: "#007AFF" }]}>
<Text style={styles.statValue}>24</Text>
<Text style={styles.statLabel}>Tasks Done</Text>
</View>
<View style={[styles.statCard, { borderLeftColor: "#FF9500" }]}>
<Text style={styles.statValue}>8</Text>
<Text style={styles.statLabel}>In Progress</Text>
</View>
<View style={[styles.statCard, { borderLeftColor: "#FF3B30" }]}>
<Text style={styles.statValue}>🔥 12</Text>
<Text style={styles.statLabel}>Day Streak</Text>
</View>
</WalkthroughableView>
</CopilotStep>
{/* STEP 4 — Quick Actions */}
<Text style={styles.sectionTitle}>Quick Actions</Text>
<CopilotStep text="Shortcuts to the most-used features." order={4} name="actions">
<WalkthroughableView style={styles.quickActionsGrid}>
{[
{ icon: "➕", label: "New Task" },
{ icon: "📷", label: "Scan Doc" },
{ icon: "💬", label: "Chat" },
{ icon: "⏰", label: "Reminder" },
].map((a) => (
<TouchableOpacity key={a.label} style={styles.quickAction}>
<View style={styles.quickActionIcon}>
<Text style={{ fontSize: 26 }}>{a.icon}</Text>
</View>
<Text style={styles.quickActionLabel}>{a.label}</Text>
</TouchableOpacity>
))}
</WalkthroughableView>
</CopilotStep>
{/* STEP 5 — Activity */}
<Text style={styles.sectionTitle}>Recent Activity</Text>
<CopilotStep text="Pick up right where you left off." order={5} name="activity">
<WalkthroughableView style={styles.activityCard}>
{[
{ icon: "✅", title: "Completed 'Design Review'", time: "2h ago" },
{ icon: "📝", title: "Added note to Project X", time: "5h ago" },
{ icon: "📅", title: "Meeting scheduled", time: "Yesterday" },
].map((item, i, arr) => (
<View key={item.title}>
<View style={styles.activityRow}>
<Text style={{ fontSize: 22, marginRight: 12 }}>{item.icon}</Text>
<View style={{ flex: 1 }}>
<Text style={styles.activityTitle}>{item.title}</Text>
<Text style={styles.activityTime}>{item.time}</Text>
</View>
</View>
{i < arr.length - 1 && <View style={styles.divider} />}
</View>
))}
</WalkthroughableView>
</CopilotStep>
{/* STEP 6 — Bottom Nav */}
<CopilotStep text="Navigate between sections using these tabs." order={6} name="navbar">
<WalkthroughableView style={styles.bottomNav}>
{[
{ icon: "🏠", label: "Home", active: true },
{ icon: "📊", label: "Stats", active: false },
{ icon: "🔔", label: "Alerts", active: false },
{ icon: "⚙️", label: "Settings", active: false },
].map((item) => (
<TouchableOpacity key={item.label} style={styles.navItem}>
<Text style={{ fontSize: 22 }}>{item.icon}</Text>
<Text style={[styles.navLabel, item.active && styles.navLabelActive]}>
{item.label}
</Text>
</TouchableOpacity>
))}
</WalkthroughableView>
</CopilotStep>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#F2F2F7" },
scroll: { paddingHorizontal: 20, paddingBottom: 20 },
header: { flexDirection: "row", justifyContent: "space-between", alignItems: "center", marginTop: 16, marginBottom: 20 },
greeting: { fontSize: 14, color: "#8E8E93" },
username: { fontSize: 22, fontWeight: "700", color: "#1C1C1E" },
avatarContainer: { position: "relative" },
avatarText: { width: 46, height: 46, borderRadius: 23, backgroundColor: "#007AFF", color: "#fff", fontWeight: "700", fontSize: 16, textAlign: "center", lineHeight: 46 },
notificationDot: { position: "absolute", top: 0, right: 0, width: 12, height: 12, borderRadius: 6, backgroundColor: "#FF3B30", borderWidth: 2, borderColor: "#F2F2F7" },
searchBar: { flexDirection: "row", alignItems: "center", backgroundColor: "#fff", borderRadius: 12, paddingHorizontal: 14, paddingVertical: 12, marginBottom: 20, elevation: 2 },
searchIcon: { fontSize: 16, marginRight: 8 },
searchPlaceholder: { color: "#8E8E93", fontSize: 15 },
statsRow: { flexDirection: "row", gap: 10, marginBottom: 24 },
statCard: { flex: 1, backgroundColor: "#fff", borderRadius: 12, padding: 14, borderLeftWidth: 4, elevation: 2 },
statValue: { fontSize: 20, fontWeight: "800", color: "#1C1C1E", marginBottom: 4 },
statLabel: { fontSize: 11, color: "#8E8E93" },
sectionTitle: { fontSize: 17, fontWeight: "600", color: "#1C1C1E", marginBottom: 12 },
quickActionsGrid: { flexDirection: "row", justifyContent: "space-between", marginBottom: 28 },
quickAction: { alignItems: "center", gap: 6 },
quickActionIcon: { width: 60, height: 60, borderRadius: 16, backgroundColor: "#007AFF20", justifyContent: "center", alignItems: "center" },
quickActionLabel: { fontSize: 11, color: "#3C3C43", fontWeight: "500" },
activityCard: { backgroundColor: "#fff", borderRadius: 16, padding: 16, marginBottom: 28, elevation: 2 },
activityRow: { flexDirection: "row", alignItems: "center", paddingVertical: 10 },
activityTitle: { fontSize: 14, fontWeight: "500", color: "#1C1C1E" },
activityTime: { fontSize: 12, color: "#8E8E93", marginTop: 2 },
divider: { height: 1, backgroundColor: "#F2F2F7" },
bottomNav: { flexDirection: "row", backgroundColor: "#fff", borderRadius: 20, paddingVertical: 12, paddingHorizontal: 10, elevation: 4 },
navItem: { flex: 1, alignItems: "center", gap: 4 },
navLabel: { fontSize: 10, color: "#8E8E93", fontWeight: "500" },
navLabelActive: { color: "#007AFF", fontWeight: "700" },
});Q: What is react-native-copilot and when would you use it? A: It's a library for building guided onboarding tours in React Native. You use it when you want to highlight specific UI elements with an overlay and tooltip, typically for first-time user onboarding or feature discovery after updates.
Q: How does walkthroughable work internally?
A: It's a Higher-Order Component that wraps any React Native component and spreads a copilot prop (containing ref and onLayout) onto the root element. This allows the library to measure the element's screen position and size to draw the overlay cutout accurately.
Q: Why must CopilotStep elements be mounted before calling start()?
A: Because start() sorts and measures all registered steps immediately. If a step isn't mounted yet, it won't be in the registry, so it will be skipped. A setTimeout delay (500–1000ms) ensures the render cycle completes before the tour begins.
Q: What causes the infinite loop bug with useEffect and start?
A: The start function from useCopilot() is recreated on every render. Listing it in useEffect's dependency array causes the effect to re-run every time the component renders, which calls start() again, which causes another render — an infinite loop. The fix is an empty [] dependency array.
Q: How does React Strict Mode affect copilot tours?
A: Strict Mode double-mounts components in development. If you use a useRef guard (hasStarted.current), the ref persists across the remount — so the second (real) mount sees hasStarted = true and skips the tour entirely. The fix is to rely on cleanup (clearTimeout) instead of a ref guard.
Q: What's the difference between view and svg overlays?
A: The view overlay uses 4 <View> rectangles placed around the target element — no animation and no extra dependencies. The svg overlay uses an SVG Path to draw the cutout — smooth animated transitions between steps, but requires react-native-svg.
Q: How do you pause a tour for user interaction?
A: Call stop() to hide the overlay, let the user interact, then call goToNth(n) with the next step's order number to resume. Do not call start() again as it restarts from step 1.
Q: What does verticalOffset do?
A: It shifts the tooltip's vertical position to compensate for the status bar or header height. Without it, the tooltip can appear shifted upward or misaligned relative to the highlighted element. A value of 36 works for most iOS/Android configurations.
Q: How do you listen for when the tour ends?
A: Use copilotEvents.on("stop", callback) from the useCopilot() hook. Always clean up with copilotEvents.off("stop", callback) in the useEffect cleanup function to prevent memory leaks.
Q: Can you highlight only some steps conditionally?
A: Yes. Use the active prop on CopilotStep. Setting active={false} causes that step to be skipped in the tour sequence while the component still renders normally.
Guide written based on react-native-copilot v3.3.3 with Expo SDK 54 / React Native 0.81