guided_flow
Interactive guided tours for Flutter — the highlighted widget stays tappable, and the tour follows the user across sheets and routes.
Most tour packages dim the screen and ask the user to press Next. The user
reads a slideshow and learns nothing by doing. guided_flow turns that around:
the spotlight has a real hole, the button underneath still works, and the tour
moves forward when the user actually performs the step.
- Tap-through spotlight — the real
onTapfires. Taps outside the hole are absorbed, so a bottom sheet cannot be swiped shut mid-tour. - Survives route changes — the overlay lives above the
Navigator, so bottom sheets, dialogs and pushed pages are spotlighted too. - Moves by itself — steps follow which anchors are on screen. You never call
next()by hand, and backing out of a sheet rewinds the tour with the user. - Small surface — three classes to learn, one line to install.
Screenshots
| Step 1 — on the page | Step 2 — inside a bottom sheet |
|---|---|
![]() |
![]() |
The user tapped the real dock button on the left, which opened the bottom sheet
on the right — the tour moved to step 2 on its own. Both shots use a custom
GuidedFlowTheme (accentColor, skipLabel: 'Lewati', a progressLabel of
PANDUAN · n DARI 10).
Install
dependencies:
guided_flow: ^1.0.0
Getting started
1. Describe the tour.
final tour = GuidedFlowController(
steps: const [
GuidedFlowStep(
anchorId: 'add-button',
title: 'Start here',
body: 'Every expense begins with this button.',
actionLabel: 'Tap the + button',
shape: BoxShape.circle,
),
GuidedFlowStep(
anchorId: 'save',
title: 'Save it',
body: 'Tapping this really saves — the tour never stood in the way.',
actionLabel: 'Tap Save',
endsTour: true,
),
],
onFinish: (ending) => debugPrint('Tour ended: ${ending.name}'),
);
2. Install the host once, above the Navigator.
MaterialApp(
builder: (context, child) => GuidedFlow(controller: tour, child: child!),
home: const HomePage(),
)
MaterialApp.builder is the important part. Anything below the Navigator
would be buried by the next route you push.
3. Wrap the widgets the steps point at.
GuidedFlowAnchor(
id: 'save',
child: FilledButton(onPressed: _save, child: const Text('Save')),
)
GuidedFlowAnchor adds no render object, no padding and no gesture, so your
layout is untouched. Without a GuidedFlow above it, it does nothing at all —
safe to leave in place.
4. Start when you are ready.
tour.start();
Nothing is drawn until the first anchor is on screen, so it is fine to start early.
How the tour moves
You will rarely call next(). Three rules keep the tour in step with the user:
| What happens on screen | What the tour does |
|---|---|
| The next step's anchor appears | Moves forward — the user just finished this step |
| The current anchor leaves while the next one is already there | Moves forward |
| The current anchor leaves and nothing follows it | After rewindDelay (1.2 s), goes back to the last step still on screen |
The delay is what stops a route transition from being mistaken for the user backing out. While the app is in the background — a camera, a file picker, a share sheet — the tour waits instead of rewinding.
Two flags cover the awkward cases:
waitForButton: true— the step never moves on by itself; the card grows a button instead. Use it when two steps live on the same screen, otherwise the second anchor is already there and the tour skips straight past the first.canRewind: false— past a point of no return. If the flow breaks there, the tour stops rather than jumping back to the beginning.
Numbering
By default a step is numbered by its position. Give several steps the same
number to present them as one visible step — useful when one screen needs two
or three spotlights in a row:
GuidedFlowStep(anchorId: 'note-field', number: 3, ...),
GuidedFlowStep(anchorId: 'note-picker', number: 3, ...),
GuidedFlowStep(anchorId: 'note-submit', number: 3, ...),
The user sees "step 3 of N" for all three.
Theming
GuidedFlow(
controller: tour,
theme: const GuidedFlowTheme(
accentColor: Color(0xFF10B981),
skipLabel: 'Lewati',
progressLabel: myProgressLabel, // (number, total) => 'LANGKAH $number/$total'
),
child: child!,
)
For a card of your own, pass cardBuilder and build whatever you like from the
GuidedFlowCardData you are handed.
Showing it only once
The package does not persist anything — that choice is yours. A common shape:
Future<void> maybeStartTour() async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool('tour_done') ?? false) return;
tour.start();
}
// in the controller:
onFinish: (ending) {
if (ending == GuidedFlowEnding.stopped) return; // the flow broke; try again later
SharedPreferences.getInstance().then((p) => p.setBool('tour_done', true));
}
GuidedFlowEnding tells you whether the user completed the tour, skipped
it, or whether it stopped because the flow broke.
Notes
- Use each anchor id once at a time. Two live anchors with the same id would fight over where the spotlight goes.
- A tour needs at least one step, and one
GuidedFlowhost per controller. - The overlay repaints every frame while a tour runs, so
pumpAndSettlenever settles in widget tests. Usepump(Duration)instead.
Example
example/ walks a four-step tour across a page, a bottom sheet and a pushed
page — the shortest way to see all of the above working together.
License
MIT
Libraries
- guided_flow
- Interactive guided tours for Flutter.

