Skip to main content

Advanced

The examples below use useWalkthroughStep for clarity, but every option also works with the WalkthroughStep component from the Usage guide. They share the same configuration.

Tuning the mask with layoutAdjustments

Sometimes the measured view doesn't quite cover what you want to highlight. layoutAdjustments lets you pad, shrink, or shift the mask:

  • addPadding: expand the mask on all sides
  • addX / addY / addWidth / addHeight: shift or resize individual edges
  • x / y / width / height: override the mask position or size entirely
  • minX / minY / maxX / maxY: clamp the mask within bounds
const { onLayout } = useWalkthroughStep({
number: 2,
contentComponent: ButtonOverlay,
layoutAdjustments: {
addPadding: 8,
},
});

The adjusted rectangle is available on the step as computedMask, which is what the displayer renders and what your content component should position against.

Programmatic control

Every function on the context (start, stop, next, previous, goTo, updateStep, registerStep) can be triggered from any component inside the provider.

Navigating functions follow a simple lifecycle: start only runs while the walkthrough is inactive, whereas next, previous and goTo only run while it is active. Calling them out of state is a no-op, so you don't need to guard them yourself.

Via useWalkthrough()

import { useWalkthrough } from "rn-interactive-walkthrough";

function SettingsScreen() {
const { goTo, stop } = useWalkthrough();

return (
<View>
<Button title="Skip tour" onPress={stop} />
<Button title="Jump to step 3" onPress={() => goTo(3)} />
</View>
);
}

Step lifecycle callbacks

onStart and onFinish fire when a step becomes active and when the walkthrough moves past it. Each receives a WalkthroughCallback with the timestamp. onBackground fires if the app goes to the background while the step is active.

const { onLayout } = useWalkthroughStep({
number: 2,
contentComponent: NearbyUsersTooltip,
onStart: ({ time }) => {
console.log("Step 2 started at", time);
},
onFinish: ({ time }) => {
console.log("Step 2 finished at", time);
},
onBackground: () => {
console.log("App went to the background during step 2");
},
});

Updating steps

updateStep(identifier, partialStep) lets you modify a registered step at runtime, useful when the position of a highlighted element changes or you want to toggle interactivity on a mask.

const { updateStep, steps } = useWalkthrough();
const searchStep = steps.find((s) => s.identifier === "search");

updateStep("search", {
computedMask: { ...searchStep?.computedMask, allowInteraction: false },
});

Global appearance

backdropColor and animationDuration can be set as props on WalkthroughProvider:

<WalkthroughProvider
backdropColor="rgba(0, 0, 0, 0.85)"
animationDuration={600}
>
<MyAwesomeApp />
</WalkthroughProvider>

Pass debug to the provider to render colored outlines around every mask and log walkthrough transitions to the console, which is very handy while building a tour.

Custom animations

Transitions are built on react-native-reanimated. The provider accepts an animations prop where every section is optional, so you can override just the pieces you care about:

import { Easing } from "react-native-reanimated";

<WalkthroughProvider
animations={{
backdrop: {
easing: Easing.inOut(Easing.quad),
},
content: {
entering: FadeInDown.duration(300),
exiting: FadeOut.duration(150),
},
}}
>
<MyAwesomeApp />
</WalkthroughProvider>;
  • backdrop.entering / backdrop.exiting: enter/exits for the backdrop pressables
  • backdrop.easing: easing curve for the mask morph between steps
  • content.entering / content.exiting / content.layout: layout animations for the step content container

Pulsing mask

Once a step is visible and has finished animating into place, the mask can pulse (scale up and down repeatedly) to draw the user's eye to the highlighted action. It is disabled by default, and is switched on through a single pulse object on the provider, so the whole behaviour lives in one place:

<WalkthroughProvider
pulse={{
enabled: true,
delay: 600,
duration: 500,
scale: 1.08,
easing: Easing.inOut(Easing.sin),
}}
>
<MyAwesomeApp />
</WalkthroughProvider>

Enabled from scratch, it uses delay: 400, duration: 400, scale: 1.05, with a fast ease-out. The pulse starts only after the mask morph has settled (delay counts from that moment), so it never fights the transition between steps. It restarts cleanly each time a step becomes active, including when you navigate backwards.

Options:

  • enabled: turn the pulse on or off (defaults to false)
  • delay: how long to wait (in ms) after the mask settles before the first beat
  • duration: how long (in ms) one bigger-to-smaller (or smaller-to-bigger) transition takes
  • scale: the peak scale of the beat, relative to the mask's base size. Use a value above 1 to grow the mask, below 1 to shrink it; 1 disables the motion
  • easing: the easing curve applied to each beat

The mask is aligned to whole pixels, so during a beat a small target can appear to move in 1px steps; at rest it always sits exactly where the morph left it.

Per-step overrides

A single step can override just some pulse options; the provider value fields you don't touch still win from the defaults. The step's pulse is merged over the provider's, per key:

const { onLayout } = useWalkthroughStep({
number: 2,
contentComponent: ButtonOverlay,
pulse: { enabled: true, scale: 1.12 },
});

Targets in scrollable containers

If a highlighted view sits inside a ScrollView, its on-screen position changes as the user scrolls. The library doesn't auto-scroll targets into view. Instead, an interactive tour guides the user to reach a target themselves, and you can keep the mask glued to a target while the user drags by re-measuring from the scroll events. measureMask() measures the live window position, so it stays accurate no matter how far you scroll:

function ScrollableScreen() {
const { currentStep } = useWalkthrough();
const scrollRef = useRef<ScrollView>(null);

const { onLayout } = useWalkthroughStep({
number: 2,
layoutLock: true,
contentComponent: ButtonOverlay,
});

return (
<ScrollView
ref={scrollRef}
onScroll={() => currentStep?.measureMask()}
onScrollEndDrag={() => currentStep?.measureMask()}
onMomentumScrollEnd={() => currentStep?.measureMask()}
scrollEventThrottle={16}
>
{/* ...long content... */}
<View onLayout={onLayout}>{/* highlighted target */}</View>
</ScrollView>
);
}

The onScroll handler keeps the mask glued to the target while the user scrolls, and the scroll-end handlers snap it to its final position once the scroll settles. layoutLock stops onLayout from re-measuring with a stale position.

Per-step animation duration

The mask morphs between steps over the provider's animationDuration (default 300ms). You can tune it per step without a global change:

useWalkthroughStep({
number: 2,
animationDuration: 150,
});

The backdrop fade still uses the provider-wide duration.