Feature spec
IntermediateCategory
personalization-ux
Build with AI
3–6 hours with FlutterFlow
Custom build
1–2 weeks custom dev
Running cost
$0–25/mo up to 1K users
Works on
Everything it takes to ship Interactive User Tutorials — parts, prompts, and real costs.
Interactive tutorials need three pieces: a spotlight overlay library (tutorial_coach_mark for Flutter), a persistence layer that remembers which users have completed each tutorial, and a trigger that only shows the tutorial when completion is not already stored. With FlutterFlow you can ship a working 7-step tutorial in 3–5 hours at $0/month. The hardest part — reliably targeting the right widget with a GlobalKey — is where first builds fail.
What Interactive User Tutorials Actually Are
Interactive user tutorials are in-app step-by-step guides that overlay the real UI with a spotlight cutout highlighting the exact button or widget being explained. A coach mark tooltip appears beside the highlighted element with a title, explanation, step counter, and navigation buttons. Unlike a walkthrough video or a help document, tutorials run inside the live app on the user's actual data — making them dramatically more effective for teaching gesture-heavy or complex-layout interfaces. The three product decisions that matter: which features to tutorial (only the non-obvious ones), when to trigger (first meaningful action, not sign-up), and how to gate re-display (check the database before starting, not a local boolean).
What users consider table stakes in 2026
- Spotlight overlay highlights exactly the UI element being described with a cutout shape (circle for icons, rectangle for cards and buttons)
- Next, Back, and Skip Tutorial buttons are visible on every step so users never feel trapped
- Step counter ('Step 3 of 7') is shown on every tooltip — users drop off more when they can't see the end
- Pulse animation on the spotlight target draws the eye to the highlighted element before the tooltip appears
- Tutorial does not restart automatically on subsequent app opens — completion is stored in the database, not just device memory
- Restart Tutorial option available in app settings for users who want to revisit the tutorial
- Works correctly on all screen sizes including tablets where widget positions differ
Anatomy of the Feature
Seven components across the UI, data, and backend layers. The overlay renderer and GlobalKey targeting are where builds fail. The trigger conditions and completion check are where behaviour breaks in production.
Tutorial overlay controller
UIFlutter's tutorial_coach_mark package (pub.dev) orchestrates the full tutorial sequence. You define a List<TargetFocus> — one entry per step — where each TargetFocus specifies the GlobalKey of the target widget, the cutout shape (CircleFocus or RectangleFocus), and the tooltip content. Calling TutorialCoachMark(targets: steps).show(context: context) starts the sequence. The library handles the overlay, the cutout rendering, and step navigation.
Note: tutorial_coach_mark is the dominant Flutter package for this pattern as of 2026 — 1K+ pub.dev likes, actively maintained. Alternatives exist (showcaseview, feature_discovery) but tutorial_coach_mark has the most complete API and best GlobalKey support.
Coach mark tooltip
UIThe tooltip widget positioned beside or below the spotlight cutout. tutorial_coach_mark renders it via a ContentTarget widget you define per step. Include: a title text widget, a body explanation widget, a step counter text ('Step 3 of 7'), a Next button, a Back button, and a Skip Tutorial TextButton. The library handles positioning to avoid edge-of-screen overflow.
Note: Keep tooltip body under 40 words per step — users tap through quickly and long text gets skipped. Save detailed explanations for a help center article linked from the skip screen.
Tutorial state machine
DataApp State variables currentTutorialStep (int, default 0) and tutorialActive (bool, default false) track the in-progress tutorial. These are set before calling TutorialCoachMark().show() and reset on skip or completion. For multi-screen tutorials, App State persistence across navigations is essential — local widget state is lost on page pop.
Note: For single-screen tutorials, page state is sufficient. For tutorials that span multiple screens, use a global provider or FlutterFlow's App State so the step index survives navigation.
Tutorial completion store
DataSupabase table tutorial_completions records which tutorials each user has completed, how far they reached, and whether they skipped. Columns: user_id, tutorial_id (a string like 'home-screen-v1'), step_reached, completed bool, skipped bool, completed_at, started_at. This is the source of truth the trigger checks before starting the tutorial — not a local flag.
Note: Store tutorial_id with a version suffix (e.g., 'home-screen-v1') so you can re-show an updated tutorial to users who completed the old version without affecting users who are mid-tutorial.
Spotlight cutout renderer
UItutorial_coach_mark renders a semi-transparent overlay across the full screen and uses a CustomPainter with Path.combine and PathOperation.difference to cut out a hole around the target widget. The cutout position is calculated from the GlobalKey's RenderBox — specifically key.currentContext?.findRenderObject() as RenderBox, then localToGlobal() for screen coordinates. An optional pulse animation (scaling the cutout radius) draws attention before the tooltip appears.
Note: The GlobalKey must be defined at the stateful widget or page level — never inside a builder function that rebuilds on state change. Each rebuild creates a new key instance, breaking the RenderBox lookup and causing the spotlight to render in the wrong position or throw a null error.
Trigger conditions
BackendLogic that decides when to start the tutorial. Common patterns: start on first app open after signup (check tutorial_completions for no row), start when a user visits a complex feature screen for the first time, or start when a specific action is completed. Implemented as a FutureBuilder or StreamBuilder in initState that queries Supabase before rendering the tutorial trigger. Only start if the completion query returns no row.
Note: Always use a post-frame callback (WidgetsBinding.instance.addPostFrameCallback) to start the tutorial — this ensures all widgets are fully rendered and their GlobalKeys are resolved before tutorial_coach_mark does its RenderBox lookup.
Settings reset option
UIA 'Restart Tutorial' button in app settings. On tap, fires a Supabase delete action removing the user's tutorial_completions row for that tutorial_id, resets App State (currentTutorialStep to 0, tutorialActive to false), and navigates back to the screen that triggers the tutorial. The next time initState runs on that screen, the completion check finds no row and starts fresh.
Note: Expose the restart option in the help or settings screen — users who want to re-watch tutorials are usually your most engaged segment, not confused beginners.
The data model
One table with a unique constraint per user-tutorial pair covers completion tracking, skip recording, and analytics. Run this in the Supabase SQL editor:
1create table public.tutorial_completions (2 id uuid primary key default gen_random_uuid(),3 user_id uuid references auth.users(id) on delete cascade not null,4 tutorial_id text not null,5 step_reached int default 0,6 completed bool default false,7 skipped bool default false,8 completed_at timestamptz,9 started_at timestamptz default now()10);1112alter table public.tutorial_completions enable row level security;1314create unique index tutorial_completions_user_tutorial_idx15 on public.tutorial_completions (user_id, tutorial_id);1617create policy "Users can manage own tutorial completions"18 on public.tutorial_completions19 for all20 using (auth.uid() = user_id)21 with check (auth.uid() = user_id);2223create policy "Service role can read all for analytics"24 on public.tutorial_completions25 for select26 to service_role27 using (true);2829create index tutorial_completions_lookup_idx30 on public.tutorial_completions (user_id, tutorial_id, completed);Heads up: The unique index on (user_id, tutorial_id) lets you upsert completion records safely — use INSERT ... ON CONFLICT DO UPDATE to write step_reached on every step advance without creating duplicate rows.
Build it — pick your path
Each tool fits this feature differently. Switch tabs to compare fit, time, the exact steps, and a copy-paste prompt.
Full control over branching tutorial logic, CMS-driven tutorial content, step-level analytics, A/B testing variants, and action-gated steps that wait for the user to tap the correct button before advancing.
Step by step
- 1Build a TutorialEngine singleton that reads tutorial configuration from a Supabase cms_tutorials table — each row defines a step with tutorial_id, step_order, target_key, tooltip_title, tooltip_body, and trigger_condition JSON
- 2Implement action-gated steps: instead of advancing on Next button tap, wait for the user to complete the specified action (tap a specific button, complete a form) before the tutorial advances — use a Dart stream or callback to detect the action
- 3Fire a Supabase insert to tutorial_step_events on every step view, advance, skip, and abandon — gives you per-step drop-off analytics without a third-party SDK
- 4Use PostHog feature flags to A/B test two tutorial sequences — variant A shows steps in order 1-7, variant B shows only steps 1, 3, 5 for a shorter flow — measure tutorial completion rate and 7-day retention per variant
- 5Build a tutorial content editor in your admin dashboard that writes to cms_tutorials — non-technical team members can update step text without an app store resubmission
Where this path bites
- CMS-driven tutorials where step content changes without an app release require careful version management — a bad config update can break the tutorial for all users simultaneously
- Action-gated steps significantly complicate the trigger architecture — every interactive element needs to know whether a tutorial is watching it, which creates coupling between UI components and the tutorial engine
Third-party services you'll need
The tutorial library and completion storage are free. Optional analytics services add step-level funnel visibility:
| Service | What it does | Free tier | Paid from |
|---|---|---|---|
| tutorial_coach_mark | Flutter package that renders spotlight cutout overlay and manages step sequences | Open-source (pub.dev), zero cost | Free |
| Supabase | Stores tutorial_completions table; queried on each screen to gate tutorial display | 2 projects, 500MB — covers tutorial completion data at small scale | $25/mo (Pro) at growth stage |
| PostHog | Tutorial funnel analytics (step completion rate, drop-off) and A/B testing tutorial variants | 1M events/mo free | Scale $450/mo (approx) — optional; free tier covers most needs |
Swipe the table sideways to see pricing.
What it costs to run
Drag through the tiers to see how your monthly bill scales with users — no surprises later.
Estimated monthly running cost
tutorial_coach_mark is free. Tutorial completion data for 100 users fits in Supabase free tier easily. Firebase Firestore Spark plan also covers this scale.
Estimates use verified 2026 pricing and assume typical usage per user. Your real bill depends on activity, storage, and third-party plans.
What breaks when AI tools build this
The failures people actually hit on their first build — the symptom, why it happens, and the exact fix.
Spotlight renders in wrong position or throws null error on GlobalKey
Symptom: tutorial_coach_mark resolves widget positions by looking up a GlobalKey and calling findRenderObject() to get the RenderBox coordinates. If the tutorial starts before the target widget is fully rendered, or if the GlobalKey was defined inside a builder function that rebuilds on state change (creating a new key instance each rebuild), the lookup returns null — the spotlight either renders at coordinates (0,0) or throws a null pointer exception.
Fix: Start the tutorial inside a WidgetsBinding.instance.addPostFrameCallback callback in initState — this guarantees all widgets are painted before the GlobalKey lookup fires. Define GlobalKey variables at the stateful widget class level (as fields), never inside build() or builder callbacks. Verify the key is passed to the widget via its key: parameter, not stored separately.
Tutorial reappears every app launch even after user completed it
Symptom: The completion check queries tutorial_completions asynchronously. If the tutorial trigger fires before the Supabase query returns a result, the default state (no completion found) causes the tutorial to start. The query returns its result a fraction of a second later — but the tutorial has already started.
Fix: Gate the entire screen's tutorial trigger inside a FutureBuilder or StreamBuilder that wraps the Supabase completion query. Only call startTutorial() inside the FutureBuilder's data callback, after confirming no completed row exists. Show a transparent loading state (or simply render the screen normally) while the query is in flight — the tutorial starting 300ms late is invisible to users; the tutorial starting when it should not is a serious UX break.
Tap-through on the spotlit widget does not register
Symptom: tutorial_coach_mark renders an overlay widget that sits above the target in the widget tree and intercepts all taps. GestureDetector callbacks on the underlying widget do not fire when the overlay is active, because the overlay receives and consumes the tap event first. Builders expect the user to be able to tap the spotlit button as part of the tutorial step — but tapping the spotlight has no effect.
Fix: Use the onTargetClick callback in each TargetFocus definition to handle the expected action instead of relying on the underlying widget's GestureDetector. Inside onTargetClick, advance to the next tutorial step and optionally fire the same action the widget would have taken (e.g., navigate to the next screen). This is how action-gated tutorials work — the tutorial controls the interaction, not the underlying widget.
Tutorial breaks mid-flow when user navigates to a different screen
Symptom: tutorial_coach_mark's TutorialCoachMark widget is mounted on a specific screen. When the user navigates to another screen (even if the tutorial expects it), the page pop removes TutorialCoachMark from the widget tree and the tutorial sequence context is lost. Returning to the original screen starts the initState check again — but if step_reached was not saved, the tutorial restarts from step 1.
Fix: Save step_reached to Supabase (via upsert) after every step advance, not only on completion. On each screen's initState, query tutorial_completions for the current tutorial_id and start the tutorial from step_reached + 1 if the tutorial is incomplete. Each screen in a multi-screen tutorial is responsible for its own steps — split the TargetFocus list by screen and each screen initialises its own TutorialCoachMark segment.
Best practices
Use WidgetsBinding.instance.addPostFrameCallback to start the tutorial — never call TutorialCoachMark().show() in initState directly, as widgets may not be rendered yet
Define GlobalKey variables as class-level fields on your stateful widget, never inside build() or builder functions — a key recreated on each rebuild breaks the RenderBox position lookup
Save step_reached to Supabase on every step advance, not just on completion — this enables resuming multi-screen tutorials and gives you per-step drop-off analytics
Tutorial each feature only once per user by default; expose a 'Restart Tutorial' option in settings rather than replaying automatically after a period of inactivity
Keep tutorial step count to 7 or fewer — each step beyond 7 increases skip rates sharply; teach the critical path, not every feature
Use CircleFocus for icon buttons and RectangleFocus with a small radius for cards and text — mismatched cutout shapes make the spotlight feel off and reduce user confidence in the highlight
Test on a physical device at every screen size you support including tablets — GlobalKey positions differ on tablets and the tooltip overflow-avoidance logic needs testing on larger screens
When You Need Custom Development
FlutterFlow with tutorial_coach_mark covers single-screen tutorials and simple multi-screen sequences. These requirements push beyond what a FlutterFlow Custom Code widget can sustain:
- Tutorial content is maintained by a non-technical content team and must be updatable without an app store resubmission — requires a CMS-driven tutorial configuration stored in a database table
- Different tutorial flows per user role (admin sees a different 8-step tutorial than end users, gated by feature flags) with A/B testing of tutorial sequence variants to measure drop-off
- Action-gated tutorials that wait for the user to complete the correct action before advancing — not just tapping Next — requiring tight integration between the tutorial engine and every interactive widget it teaches
- Per-step analytics (time on step, drop-off rate, replay rate) fed into a dashboard for the product team to optimise the tutorial sequence over time
RapidDev builds these features for production
Real auth, database, and payments — fixed price, delivered in weeks. You own 100% of the code.
Frequently asked questions
Which Flutter package is best for in-app tutorials?
tutorial_coach_mark is the leading package for spotlight-style coach marks in 2026 — it has the most complete API, active maintenance, and handles GlobalKey targeting reliably. Alternatives include showcaseview (similar API, slightly different tooltip positioning) and feature_discovery (Material Design-style discovery cards). tutorial_coach_mark is the right default for most apps.
How do I prevent the tutorial from showing again after completion?
Check the tutorial_completions table in Supabase before triggering the tutorial. In your screen's initState, run a Supabase query for the current (user_id, tutorial_id) pair. Only call TutorialCoachMark().show() if no completed row is found. Save completed=true, completed_at=now() to tutorial_completions inside the TutorialCoachMark onFinish callback. Never rely on a device-local flag — it resets when the user reinstalls the app.
Can the tutorial span multiple screens?
Yes, but it requires splitting the tutorial into per-screen segments and using App State to persist the step index across navigations. Each screen defines a TutorialCoachMark for its own steps and reads step_reached from Supabase on initState to start from the right position. Save step_reached to the database on every step advance so the tutorial can resume if the user navigates away mid-flow.
How do I highlight a specific button in the tutorial?
Assign a GlobalKey to the target widget and pass it to a TargetFocus in your tutorial step list. The GlobalKey must be defined as a class-level field on your stateful widget — not inside build() or a builder function. tutorial_coach_mark calls findRenderObject() on the key to calculate the screen position of the cutout. Use CircleFocus for icon buttons and RectangleFocus with rounded corners for larger widgets.
Can I let users restart the tutorial from settings?
Yes. Add a 'Restart Tutorial' button in your settings screen. On tap, run a Supabase delete query removing the user's tutorial_completions row for that tutorial_id, reset App State variables (currentTutorialStep=0, tutorialActive=false), and navigate to the screen that triggers the tutorial. The next time that screen's initState runs, the completion check finds no row and starts fresh.
Does tutorial_coach_mark work on tablets?
Yes — it uses RenderBox screen coordinates which are resolution-independent. However, widget positions differ on tablets (larger layout, different font sizes), so tooltip placement can overflow the screen edge differently than on phones. Test on at least one tablet device and one phone during development. The library's tooltip overflow avoidance handles most cases but very wide tooltips on small phones or very tall tooltips on short tablets need manual content trimming.
How do I track which tutorial steps users drop off at?
Upsert step_reached to tutorial_completions on every step advance (not just completion). Then query MAX(step_reached) grouped by tutorial_id across your user base to find the highest step users reach on average. Steps with a sharp drop-off in step_reached distribution are the ones where users give up. For richer analytics (time per step, replay rate), fire a PostHog event on every step view — PostHog's free tier (1M events/mo) covers most early-stage apps.
Can the tutorial advance automatically when the user completes an action?
Yes, but it requires more than the default tutorial_coach_mark setup. Use the onTargetClick callback in each TargetFocus to intercept taps on the spotlit widget — advance the tutorial inside this callback after the action fires. For non-tap triggers (form submission, data load), use a Dart callback or stream that fires when the action completes, then call tutorialCoachMark.next() programmatically. This is action-gated tutorial logic and is the most effective tutorial UX — but also the most complex to implement.
Need this feature production-ready?
RapidDev builds interactive user tutorials into real apps — auth, database, payments — at $13K–$25K.
Book a free consultation30-min call. No commitment.