hint_kit

pub package pub points license: MIT

Flutter toolkit for tooltips, persistent hints, and guided tours using one overlay engine, placement resolver, theme, and controller pattern.

It supports disabled widgets, route-spanning tours, real spotlight passthrough, reusable themes, animations, persistence, localization, analytics, and accessibility β€” with zero runtime dependencies.

Screenshots

Hints, designs and animations A four-step guided tour
A hint on a disabled button, three of the ready-made designs, the transitions, a hand-drawn caret and a show-once callout A tour: the spotlight travels between targets, step 3 expands a panel first, and step 4 waits for another route

Both recorded on a device from example/, captioned as they go. The tour is the one worth watching: the spotlight travels between targets rather than cutting, step 3 expands a panel before it appears, and step 4 waits until you push the route it lives on.

A hint on a disabled button The branded preset The glass preset
A dark tooltip pointing at a disabled Check in button A tinted bubble using the branded preset A translucent bubble over a blurred background
A tour step, spotlight and all A retinted, lighter scrim Dark mode, unconfigured
A passthrough tour step with a hole cut in the scrim The same tour over a navy scrim at 55%, with the controls that set it The same screen in dark mode with an inverted bubble

Every one of these is the example app on a device β€” nothing is mocked up. Run it yourself: cd example && flutter run.

Features

  • 🚫 Disabled widgets β€” show hints even when the child is disabled.
  • πŸ—ΊοΈ Guided tours β€” ordered steps that can continue across routes.
  • πŸ”¦ Spotlight β€” real scrim hole with optional hit-test passthrough.
  • 🎨 10 presets β€” material, minimal, soft, contrast, branded, sharp, card, glass, cupertino, adaptive.
  • ✨ Animations β€” scale, fade, pop, slide, none, or custom.
  • πŸ’Ύ Persistence β€” show-once hints and interrupted-tour resume.
  • 🌍 Localization β€” custom labels and progress formatting.
  • πŸ“Š Analytics β€” app-wide hint and tour observer.
  • β™Ώ Accessibility β€” bubbles spoken as live regions, focus handling, text scaling, high contrast, reduced motion, and Esc support.
  • πŸ“¦ Zero dependencies β€” pure Flutter.

Install

dependencies:
  hint_kit: ^1.3.0

or:

flutter pub add hint_kit
import 'package:hint_kit/hint_kit.dart';

Quick start

Hint

Hint(
  message: 'You need an active shift to check in',
  child: ElevatedButton(
    onPressed: null,
    child: const Text('Check in'),
  ),
);

Hint supports tap, longPress, hover, focus, manual, onAppear, and secondaryTap triggers.

Controller

final controller = HintController();

Hint(
  controller: controller,
  triggers: const {HintTrigger.manual},
  message: 'Saved successfully',
  child: saveButton,
);

controller.show();
controller.hide();
controller.toggle();

Guided tours

Wrap the application with TourScope and mark the steps with HintTarget:

TourScope(
  child: MaterialApp(
    home: HintTarget(
      tour: 'onboarding',
      order: 1,
      title: 'Start here',
      description: 'Tap this button to continue.',
      child: const Text('Check in'),
    ),
  ),
);

Start the tour with:

Tour.read(context).start('onboarding');

Control a tour with:

tour.next();
tour.previous();
tour.skip();
tour.finish();
tour.cancel();

Tours can wait for targets that are not currently mounted, making route-spanning onboarding possible.

Conditional steps

A step that is simply not built is one the tour waits for β€” it cannot tell "this user does not have that feature" from "this user has not opened that screen yet". Say so explicitly:

HintTarget(
  tour: 'onboarding',
  order: 3,
  enabled: user.canApproveShifts,
  child: approveButton,
)

The child renders either way; only the tour ignores it. An opted-out step is subtracted from tourLengths, so the card reads "3 of 4" rather than "3 of 5" with one that never comes.

For the cases you cannot predict β€” a target deleted in a refactor, a screen that fails to load β€” give the scope a deadline:

TourScope(
  stepTimeout: const Duration(seconds: 5),
  onStepUnavailable: (tour, index) => analytics.log('tour_step_missing', index),
  child: const MyApp(),
);

It is null by default, deliberately: waiting forever is what makes route-spanning tours work.

Spotlight & passthrough

HintTarget(
  tour: 'onboarding',
  order: 1,
  spotlight: SpotlightShape.circle,
  passthrough: true,
  child: checkInButton,
)

With passthrough: true, interaction inside the spotlight can reach the actual target.

Show once

Display a hint only once:

Hint(
  showOnce: 'payslip-tip',
  triggers: const {HintTrigger.onAppear},
  message: 'Payslips live here now',
  child: payslipTab,
);

For persistent storage, assign your own TourStorage implementation:

TourScope(
  storage: myStorage,
  child: const MyApp(),
);

HintRegistry.instance.storage = myStorage;

Both wires are needed, and they are separate on purpose: TourScope.storage persists tour progress, HintRegistry.instance.storage persists showOnce hints. Setting one and not the other warns in debug.

Theming

Configure a shared theme with HintThemeData:

MaterialApp(
  theme: ThemeData(
    extensions: const [
      HintThemeData(
        preset: HintPreset.branded,
        maxWidth: 320,
      ),
    ],
  ),
);

Or configure an individual hint:

Hint(
  theme: const HintThemeData(
    preset: HintPreset.glass,
  ),
  message: 'This is a glass-style hint',
  child: button,
);

Themes control colors, borders, radius, elevation, padding, arrow, placement spacing, typography, transitions, blur, scrim, and spotlight settings.

Rich bubbles

Use contentBuilder for custom interactive content:

Hint(
  interactive: true,
  triggers: const {HintTrigger.hover, HintTrigger.tap},
  contentBuilder: (context) => Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      const Text('Payslip #4821'),
      TextButton(
        onPressed: _download,
        child: const Text('Download'),
      ),
    ],
  ),
  child: chip,
);

Beacon

For a lightweight alternative to a full tour:

Beacon(
  title: 'Duplicate a shift',
  message: 'Long-press a shift to copy it.',
  pulseCount: 3,
  child: const Icon(Icons.calendar_month),
);

API surface

Type Purpose
Hint Tooltip / hint widget
HintController Programmatic hint control
HintTarget Guided-tour step
TourScope Tour configuration and storage
TourController Start and control tours
HintThemeData Shared and per-widget styling
HintPreset Ready-made designs
HintQueue Sequential hints without a tour
Beacon Pulsing hint indicator
HintObserver Analytics / lifecycle events
TourStorage Persistence for tours and show-once hints
resolvePlacement Pure placement resolver

Requirements

  • Flutter 3.24+
  • Dart 3.5+
  • Android, iOS, web, macOS, Windows, Linux

Notes

  • Hint requires an Overlay ancestor, normally provided by MaterialApp, CupertinoApp, or Navigator.
  • An ancestor IgnorePointer or AbsorbPointer can prevent the hint from receiving pointer events.
  • Lazy off-screen list items cannot be targeted until they are built.
  • The default InMemoryTourStorage does not persist between app launches.
  • A Beacon with unlimited pulsing can keep widget tests from settling; use pulseCount or disable auto-start.

License

MIT

Libraries

hint_kit
Tooltips, hints and guided tours built on a single overlay engine.
testing
Helpers for testing code that uses hint_kit.