hintful
Hints & onboarding tours for Flutter. Spotlight targets, tooltips, coach marks, guided walkthroughs — a single source of truth for teaching users your product.
You wrap one widget in HintTarget, describe what to show in a HintTour,
and the engine renders, repositions and remembers it — without a single
hand-written overlay, scroll math or duplicated per-screen styling.
See it in action
A hintful tour running over the example app
Recorded on the example/ app. Try it live:
fellmonkey.github.io/hintful.
Index
Start here — See it in action · Why hintful · What you write · Fast
What it does — Zero-config, then total control · Diagnosis over mystery · Accessibility · Works anywhere · Features · Server-driven tours
Install & docs — Getting started · Documentation · best practices · FAQ · Performance
Why hintful
Every Flutter hint/tour library you've seen is built on the same two ideas:
GlobalKey + a full-screen OverlayEntry that the library manually positions,
scrolls and lays out. That is exactly where tours break: the tooltip drifts a
pixel off or covers the control it points at, the overlay goes off-screen
mid-scroll and dies with This widget has been unmounted, and on a first run it
silently gives up because the target isn't built yet.
hintful throws that model away.
What's different
Old way (GlobalKey + overlay) |
hintful |
|---|---|
| Manual position / scroll / re-layout | CompositedTransform — tooltip and scrim follow the target every frame, zero scroll math, overflow impossible |
| References to widget contexts | Registry by id — HintTarget(id: 'filters') registers/unregisters itself; nothing to unmount |
| "Wait until the widget is built" by hand | Wait-for-target — a tour waits for a deferred target instead of dying |
| Per-hint hard-coded styling | ThemeExtension — hint inherits your design system, light and dark, from Theme.of |
| Tied to Bloc/Riverpod/… | Framework-agnostic core — vanilla ValueListenable<HintState>, no state-management imports |
| Overlay mounted even when idle | Zero-idle cost — zero engine widgets in the tree until a tour actually starts |
Zero-idle is about the engine: no overlay, entry or listener exists until a tour
starts. The thin HintTarget wrapper around your widget is the only idle
footprint — that's the 4 nodes in the S1 row of the benchmark table below.
What you write
// 1. Wrap the thing you want to explain
HintTarget(
id: 'exerciseSelector',
child: ExerciseSelector(),
)
// ...or the one-liner sugar: ExerciseSelector().withHint('exerciseSelector')
// 2. Declare the tour — data, not widgets
final introTour = HintTour(
id: 'intro',
steps: [
HintStep(
targetId: 'exerciseSelector',
title: 'Pick a movement',
description: 'Filter by muscle, equipment or name.',
),
HintStep(
targetId: 'addSet',
title: 'Log your set',
description: 'Weight × reps, one tap.',
),
],
);
// 3. Wire once, show once
final controller = HintController(
overlayHostBuilder: defaultOverlayHost(),
);
controller.start(introTour);
No GlobalKey, no OverlayEntry, no ScrollController, no manual position.
That's the whole tour — and it already handles light/dark, scrolling and
deferred targets.
Localizing? Swap title/description for titleBuilder/descriptionBuilder
((c) => AppLocalizations.of(c)!.introTitle): the copy stays in your AppTours
file and the BuildContext arrives from the overlay.
// Just one tip? No tour needed:
controller.showHint(
HintStep(targetId: 'addSet', title: 'Swipe left to delete a set'),
);
Fast — measured, not promised
One scene, three libraries, profile Android emulator — recorded by CI into
benchmark/benchmarks.json, rendered straight from that file into the table
below — one source of truth for every number. Table, charts, methodology:
Performance.
Zero-config, then total control
Out of the box, title/description steps render in a default tooltip under
a default theme — the tour above is already complete. When you need more, the
API grows rung by rung, each optional: HintTheme styles → HintTooltipLabels
(button texts, waiting placeholder, screen-reader announcements) →
titleBuilder/descriptionBuilder for l10n → a fully
custom tooltip through tooltipBuilder. Your design system, your call.
Diagnosis over mystery
When a hint doesn't show, you'll know why in one log line:
[hintful] statsIntro step 2 not shown: timeout (target 'statsPeriodSelector') — target 'statsPeriodSelector' did not appear within 0:00:03.000000
Not "it just didn't appear." If you typo a targetId, hintful tells you loudly in
debug — with the closest candidates.
The reasons and their fixes: FAQ §1; wiring your own handler for analytics: best practices §12.
Accessibility, on by default
- Screen readers: every step is announced as "Step N of M:
Libraries
- engine/controller
- engine/diagnostics
- engine/labels
- engine/machine
- engine/motion
- engine/overlay/overlay_engine
- engine/overlay/pulse_painter
- engine/overlay/scrim_painter
- engine/overlay/tooltip_placement
- engine/overlay/tooltip_tail
- engine/position_resolver
- engine/registry
- engine/specs
- engine/store
- engine/theme/hint_theme
- engine/tour_factory
- hintful
- hintful — the package's public contract.
- widgets/default_tooltip
- widgets/hint_target
- widgets/hint_target_ext
- widgets/tour_offer