Tour
A guided walkthrough — the screen dims, a hole opens around one control, and a card beside it explains it.
Installation
Tour ships with the library — install it once, then import:
import { Tour } from "@delacour/react-native-ui/tour";Or copy the source into your project, to own and edit it:
bunx delacour@latest add tourUsage
Wrap the screen in a Tour and each control you want to point at in a Tour.Step.
import { Button } from "@/components/ui/button";
import { Tour, useTour } from "@/components/ui/tour";
import { View } from "react-native";
function StartTour() {
const tour = useTour();
return <Button onPress={() => tour.start()}>Show me around</Button>;
}
export default function Example() {
return (
<Tour onFinish={markSeen} onSkip={markSeen}>
<View className="flex-1 justify-center gap-4 p-4">
<Tour.Step order={1} title="Search" description="Find anything in your library.">
<SearchButton />
</Tour.Step>
<Tour.Step order={2} title="New note" description="Start writing." shape="circle">
<Fab />
</Tour.Step>
<StartTour />
</View>
</Tour>
);
}Steps are sorted by order, so gaps are fine — 10, 20, 30 leaves room to insert one later. A
Tour.Step that is not mounted is not in the tour.
Opening it
Three ways, the same as any disclosure in the library:
<Tour defaultOpen>…</Tour>
<Tour isOpen={isOpen} onOpenChange={setOpen}>…</Tour>
useTour().start(); // or start(order) to begin part-way throughThe tour keeps no record of having been seen. onFinish (Done on the last step) and onSkip
(Skip, a scrim tap, Android back or VoiceOver's escape) are where an app writes that down.
Hole shape
rect (the default) is a rounded rectangle grown by padding; circle is a circle around the
target's larger side, for a floating button. Set it on the tour for every step, or on one step.
Between steps the old hole fades closed while the new one opens, and the card glides to its new
place.
<Tour.Step order={2} shape="circle" padding={6} title="New note">
<Fab />
</Tour.Step>Placement
auto puts the card below the hole when it fits inside the safe area, and above otherwise. top
and bottom are honoured when they fit and flip when only the other side does. The card is
centred on the hole and clamped to the screen's gutters, at most 360 points wide.
<Tour.Step order={3} placement="top" title="Composer">…</Tour.Step>Interactive
By default the tour is a Modal: it covers the status bar and the tab bar and swallows every
touch. isInteractive draws it inside the tour's own root instead and lets touches inside the
hole reach the control — whose handler then advances the tour.
function LikeButton() {
const tour = useTour();
return (
<Button
onPress={() => {
like();
if (tour.isOpen && tour.step === 1) tour.next();
}}
>
Like
</Button>
);
}
<Tour isInteractive>
<Tour.Step order={1} title="Like it" description="Tap the heart.">
<LikeButton />
</Tour.Step>
</Tour>An interactive tour can only dim what is inside it. Put Tour around the whole screen — a status
bar or tab bar outside it stays lit.
Scrolling a step into view
onStepChange fires before the step is measured. Scroll the target on screen there, without
animation — the tour measures one frame later.
<Tour onStepChange={(order) => scrollRef.current?.scrollTo({ y: offsets[order], animated: false })}>
…
</Tour>Labels
Every string on the card can be replaced. progress is the visible "2 of 5"; announcement is
what a screen reader hears before the title.
<Tour
hasSkip={false}
labels={{
next: "Suivant",
back: "Retour",
finish: "Terminé",
progress: (current, total) => `${current}/${total}`,
announcement: (current, total) => `Étape ${current} sur ${total}`,
}}
>
…
</Tour>Accessibility
- Focus moves to the card on every step, and the card is announced as one element: "Step 2 of 5. Search. Find anything in your library."
- The card is
accessibilityViewIsModal, so VoiceOver stays inside it. A non-interactive tour also hides the app beneath from assistive technology; an interactive one leaves it reachable. - VoiceOver's escape gesture and Android back skip the tour when
isDismissible(the default). - Under reduce motion the card jumps between steps instead of gliding. The hole only fades, so it fades either way.
API Reference
Anatomy
| Part | What it is |
|---|---|
Tour | Owns the open state and the current step; draws the mask, the scrim and the card |
Tour.Step | Wraps one control. Registers itself and is measured for the hole |
useTour() | Reads and drives the enclosing tour |
Tour
Prop
Type
Tour.Step
Extends ViewProps. shape, padding, radius and placement override the tour's for this step.
Prop
Type
useTour
const { isOpen, step, index, total, start, next, back, skip, finish, goTo } = useTour();Throws outside a Tour.
Exports
import {
Tour,
type TourProps,
type TourStepProps,
type TourLabels,
TOUR_DEFAULT_LABELS,
resolveCardPlacement,
useTour,
} from "@delacour/react-native-ui/tour";