hintful 0.6.1
hintful: ^0.6.1 copied to clipboard
Onboarding tours and tooltips for Flutter. Registry-based targets, theme-aware coach marks, smart positioning, accessible by default — zero cost when idle.
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.
Why another hint library? #
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 model is precisely why tours break:
- tooltips overflow by 1px, or overlap their own target;
- the overlay jumps off-screen mid-scroll, then crashes with
This widget has been unmountedwhen the target scrolls out of view; - on the first run — when targets are still being built — the tour silently dies because the widget it wants doesn't exist yet;
- every hint hard-codes its own colors, so the tour never matches your design system;
- pick a state-management stack and you're locked into it forever.
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 |
What you write #
// 1. Wrap the thing you want to explain
HintTarget(
id: 'exerciseSelector',
child: 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.
// 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 and rendered into the table below by the bot, so
the numbers have a single source of truth. 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) → 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.
Accessibility, on by default #
- Screen readers: every step is announced as "Step N of M: