overmark 0.2.0
overmark: ^0.2.0 copied to clipboard
Spotlight coach marks for Flutter onboarding tours. Dims the screen, highlights any widget through a cut-out, and explains it in a fully customizable card. Zero dependencies.
Overmark #
Spotlight coach marks for Flutter onboarding tours. Dim the screen, cut a hole around a widget, explain it in a card, move on to the next one — or wait until the user actually taps the highlighted control.
Default theme · Custom theme
Action step · Whisper tips · Lazy list + prepare
- No dependencies. Just Flutter. Nothing is pulled into your app.
- Teach by doing. Action steps pass taps through the hole to the real
widget and wait for
completeAction. - Fits any theme. Every colour and text style falls back to the ambient
ThemeData, so the default look is neutral in light and dark alike. - Customisable to the pixel. Restyle through
OvermarkTheme, or replace the card entirely with your own widget. - Behaves like a real route. The system back gesture ends the tour, and the call returns a future telling you how it ended.
- Keeps up. The spotlight tracks its anchor through rotation, resize, and scrolling, waits for lazy-list items to mount, and docks the card around the hole with an optional leader line.
- Premium spotlight. Soft rim / glow, whisper (non-blocking) tips, card Pop-in, and optional haptics — all opt-in.
Install #
dependencies:
overmark: ^0.2.0
Quick start #
Attach a GlobalKey to each widget you want to highlight, then start a tour.
import 'package:overmark/overmark.dart';
final GlobalKey searchKey = GlobalKey();
final GlobalKey shelfKey = GlobalKey();
// ...
TextField(key: searchKey);
// ...
final OvermarkOutcome outcome = await Overmark.show(
context,
steps: <OvermarkStep>[
OvermarkStep(
anchorKey: searchKey,
title: 'Find your next book',
message: 'Search by title, author, or topic.',
),
OvermarkStep(
anchorKey: shelfKey,
title: 'Tap any cover',
message: 'Every book is a short visual summary.',
),
],
);
Steps whose anchors are not mounted yet are waited on (see readiness below). If nothing becomes ready in time, the tour is not shown.
outcome is one of completed, skipped, dismissed, or notShown.
Action step recipe #
Use an action gate when the user should press the real control — not Next.
final GlobalKey startKey = GlobalKey();
final OvermarkController controller = OvermarkController();
FilledButton(
key: startKey,
onPressed: () {
startTimer(); // your real work
controller.completeAction(); // advance the tour
},
child: const Text('Start'),
);
await Overmark.show(
context,
controller: controller,
steps: <OvermarkStep>[
OvermarkStep(
anchorKey: startKey,
title: 'Start a session',
message: 'Tap Start to begin.',
gate: OvermarkStepGate.action,
),
],
);
While the gate is open, the hole passes taps through to the button, Next is
hidden, and controller.next() is a no-op. Call completeAction from the
control's onPressed / onTap after your real work. Skip, Back, and
finish() still work.
Readiness & lazy lists #
Anchors that are not laid out yet are polled until they appear (default 3s).
For list items that need a scroll first, use prepare:
OvermarkStep(
anchorKey: itemKey,
title: 'Pinned item',
message: 'Scrolls into view before the spotlight lands.',
prepare: (BuildContext context) async {
await Scrollable.ensureVisible(
itemKey.currentContext!,
alignment: 0.5,
);
},
)
Configure timeouts on OvermarkConfig (readinessTimeout,
readinessInterval) or per step with readinessTimeout.
Show it only once #
showOnce is the usual first-run pattern. Without a storage it remembers for the
current launch only:
await Overmark.showOnce(
context,
tourId: 'home_tour_v1',
steps: steps,
);
To remember across launches, hand it somewhere to write. Overmark has no
dependencies, so the adapter lives in your app — with shared_preferences it is
six lines:
class PrefsOvermarkStorage implements OvermarkStorage {
@override
Future<bool> hasShown(String tourId) async =>
(await SharedPreferences.getInstance()).getBool('tour_$tourId') ?? false;
@override
Future<void> markShown(String tourId) async =>
(await SharedPreferences.getInstance()).setBool('tour_$tourId', true);
}
await Overmark.showOnce(
context,
tourId: 'home_tour_v1',
storage: PrefsOvermarkStorage(),
steps: steps,
);
Mark policy #
| Policy | When the tour is recorded as shown |
|---|---|
onStart (default) |
After the first step is ready, just before the route is pushed |
onComplete |
Only when the outcome is completed |
onAction |
On the first completeAction (or on complete if there is no action step) |
await Overmark.showOnce(
context,
tourId: 'home_tour_v1',
markPolicy: OvermarkMarkPolicy.onComplete,
storage: PrefsOvermarkStorage(),
steps: steps,
);
Bumping the tourId (home_tour_v2) is how you re-run a tour after redesigning
a screen.
Theming #
Leave a field null and it is derived from Theme.of(context). Set only what you
want to change:
await Overmark.show(
context,
theme: OvermarkTheme(
scrimColor: const Color(0xFF1B0E2B).withAlpha(220),
spotlightBorderColor: const Color(0xFFFFC857),
spotlightRadius: 28,
cardRadius: 28,
buttonColor: const Color(0xFFFFC857),
),
steps: steps,
);
| Group | Fields |
|---|---|
| Spotlight | scrimColor, spotlightBorderColor, spotlightBorderWidth, spotlightRadius, spotlightPadding |
| Soft mask | spotlightRimBlur, spotlightRimColor, spotlightGlowBlur, spotlightGlowColor |
| Pulse | spotlightPulse, spotlightPulseAmplitude, spotlightPulseDuration |
| Card motion | cardMotion (none / fade / fadeSlide / fadeScale), cardMotionOffset, cardMotionScaleBegin |
| Card | cardColor, cardRadius, cardPadding, cardMargin, cardGap, cardShadows |
| Leader | showLeaderLine, leaderLineColor, leaderLineWidth |
| Text | titleStyle, messageStyle, skipStyle, buttonLabelStyle |
| Button | buttonColor, buttonPadding, buttonRadius |
| Progress dots | indicatorSize, indicatorSpacing, indicatorColor, indicatorActiveColor |
Translate the controls with OvermarkLabels:
labels: const OvermarkLabels(
next: 'Lanjut',
previous: 'Kembali',
skip: 'Lewati',
done: 'Mengerti',
),
Behaviour #
config: const OvermarkConfig(
advanceOnBarrierTap: true, // tap scrim to move on (ignored during action gates / whisper)
showSkip: true,
showPrevious: true, // Back on the default card
showIndicator: true,
animationDuration: Duration(milliseconds: 320),
animationCurve: Curves.easeOutCubic,
cardMotionDuration: null, // null → animationDuration
cardMotionCurve: null, // null → strong ease-out Pop-in
transitionDuration: Duration(milliseconds: 180),
ensureVisible: true,
scrollAlignment: 0.5, // 0.0 leading, 0.5 center
readinessTimeout: Duration(seconds: 3),
placement: OvermarkCardPlacement.auto,
dismissible: true, // false locks system / gesture back until Skip, Next, or finish
useRootNavigator: true,
pointerMode: OvermarkPointerMode.modal, // or .whisper for non-blocking tips
haptics: OvermarkHaptics.off, // .selection or .light
),
Whisper mode #
// Non-blocking tip — taps pass through the scrim; the card stays interactive.
await Overmark.show(
context,
config: const OvermarkConfig(pointerMode: OvermarkPointerMode.whisper),
theme: OvermarkTheme(scrimColor: Colors.black.withAlpha(55)),
steps: steps,
);
Hide the arrow between the card and the spotlight with theme:
theme: const OvermarkTheme(showLeaderLine: false),
With dismissible: false, the system back button and iOS/Android back gesture do
not end the tour. The user still leaves through Skip, Next/Done, completeAction,
or controller.finish().
Per-step overrides #
Any step can override the spotlight shape, corner radius, padding, gate, and prepare callback:
OvermarkStep(
anchorKey: profileTabKey,
title: 'Your progress',
message: 'Streaks and stats live here.',
shape: OvermarkShape.circle,
padding: const EdgeInsets.all(12),
)
Your own card #
When theming is not enough, replace the card for a step. You get the tour state
and the navigation callbacks — including isAwaitingAction and
completeAction:
OvermarkStep(
anchorKey: shelfKey,
cardBuilder: (BuildContext context, OvermarkStepDetails details) {
return MyCard(
step: details.index + 1,
of: details.stepCount,
isLast: details.isLast,
awaitingAction: details.isAwaitingAction,
anchor: details.anchorRect,
onNext: details.next,
onBack: details.previous,
onClose: details.skip,
onActionDone: details.completeAction,
);
},
)
Driving the tour yourself #
final OvermarkController controller = OvermarkController();
Overmark.show(context, controller: controller, steps: steps);
controller.next();
controller.previous();
controller.completeAction();
controller.skip();
controller.finish();
Callbacks are available too: onStepChanged, onSkip, and onFinish.
Version support #
| Dart | >=3.4.0 <4.0.0 |
| Flutter | >=3.22.0 |
| Package platforms | Android, iOS, web, macOS, Windows, Linux |
Pure Dart and Flutter widget tests run in CI on the oldest supported Flutter and on current stable.
Example #
A runnable demo lives in example/: a first-run tour with an action
step, an Overmark lab (whisper, action, mark policies, locked back, leader
line), a Sessions tab for lazy-list + prepare, a custom theme, a light/dark
toggle, and a shared_preferences storage adapter.
The GIFs at the top were recorded from that app: default and custom themes, then action steps, whisper tips, and scrollable / lazy anchors.
cd example
flutter run
License #
MIT