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.

pub package license: MIT

Default Overmark tour    Custom-themed Overmark tour

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.