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.
Default theme ยท Custom theme
- No dependencies. Just Flutter. Nothing is pulled into your app.
- 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, and scrolls off-screen anchors into view for you.
Install
dependencies:
overmark: ^0.1.2
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 anchor is not on screen are dropped, and the tour is skipped entirely when that leaves nothing to show โ so it is safe to call on a screen whose content is still loading.
outcome is one of completed, skipped, dismissed, or notShown.
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,
);
The tour is marked as shown before it is displayed, so a crash midway through never traps a user in a tour that replays on every launch.
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 |
| Card | cardColor, cardRadius, cardPadding, cardMargin, cardGap, cardShadows |
| 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', skip: 'Lewati', done: 'Mengerti'),
Behaviour
config: const OvermarkConfig(
advanceOnBarrierTap: true, // tap anywhere to move on
showSkip: true, // skip control before the last step
showIndicator: true, // progress dots
animationDuration: Duration(milliseconds: 320),
animationCurve: Curves.easeOutCubic,
transitionDuration: Duration(milliseconds: 180),
ensureVisible: true, // scroll an off-screen anchor into view
dismissible: true, // system back ends the tour
useRootNavigator: true,
),
Per-step overrides
Any step can override the spotlight shape, corner radius, and padding:
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:
OvermarkStep(
anchorKey: shelfKey,
cardBuilder: (BuildContext context, OvermarkStepDetails details) {
return MyCard(
step: details.index + 1,
of: details.stepCount,
isLast: details.isLast,
anchor: details.anchorRect,
onNext: details.next,
onBack: details.previous,
onClose: details.skip,
);
},
)
Driving the tour yourself
final OvermarkController controller = OvermarkController();
Overmark.show(context, controller: controller, steps: steps);
controller.next();
controller.previous();
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 |
| Platforms | Android, iOS, web, macOS, Windows, Linux |
Pure Dart and Flutter, so every platform is supported. CI runs the test suite on the oldest supported Flutter and on the current stable.
Example
A runnable demo lives in example/: a first-run tour, a replay with a
completely different theme, a light/dark toggle, and a shared_preferences
storage adapter. The GIFs above were recorded from that app โ default look on
the left, custom OvermarkTheme on the right.
cd example
flutter run
License
MIT
Libraries
- overmark
- Spotlight coach marks for Flutter onboarding tours.