spotlight_guide 0.1.5 copy "spotlight_guide: ^0.1.5" to clipboard
spotlight_guide: ^0.1.5 copied to clipboard

A Flutter overlay guide for spotlighting widgets with dynamic targets, reveal scrolling, custom hints, and robust widget tests.

Spotlight Guide #

SpotlightGuidePortal is a reusable onboarding overlay for highlighting one or more widgets on a page. It supports target holes, ready-made and custom hint UI, connected bubble arrows, pointer widgets, RTL-aware anchor positions and semantic placement, automatic placement, reveal scrolling, lazy-list preparation, and multiple hints in the same step.

Preview #

Basic flow Pointer hint Same-step hints
Basic spotlight guide flow Built-in tap pointer hint Several hints in one step
Same-step auto scroll Lazy target reveal Barrier dismiss
Same-step automatic scroll Lazy target reveal scroll Barrier tap dismiss modes
Custom anchors and groups Horizontal auto Target decoration
Custom anchors and repeated target groups Horizontal auto placement choosing left and right arrows Target decoration guide scenarios

Highlights #

  • Static or runtime guide steps through SpotlightGuidePortalController.
  • Ready-made text hints and a built-in tap pointer for quick onboarding.
  • Multiple hints in one step, including sequential same-step auto scroll.
  • Dynamic API-driven steps with skip/wait behavior for missing targets.
  • Lazy-list targets using onReveal before measurement.
  • Repeated target ids highlighted as a group, with optional anchorId.
  • Shape-aware target hole decoration, custom anchor positions, interactive bubble arrows, barrier style, and hints.

When To Use It #

Use spotlight_guide when a guide needs to survive real app complexity: scrolling pages, lazy lists, optional API-driven targets, repeated list rows, multiple highlights in one step, or custom visual treatment around the target hole. For a tiny fixed three-step tour, start with SpotlightGuideTextHint and only move to lower-level widgets when the design needs them.

Public Entry #

Import the public entry:

import 'package:spotlight_guide/spotlight_guide.dart';

The implementation lives here:

lib/spotlight_guide.dart
lib/src/api/
lib/src/runtime/
lib/src/hints/
lib/src/layout/
lib/src/painting/
lib/src/utils/

Runtime files are small collaborators rather than page code. For example, runtime/reveal_scroll_strategy.dart owns reveal scroll decisions, while api/portal.dart keeps the Flutter widget and State orchestration together.

Documents #

README.md
  Overview and common usage.

CONTRIBUTING.md
  Maintainer rules, architecture map, invariants, and required verification.

CHANGELOG.md
  Current component contract and maintenance notes.

doc/reference.md
  Public parameter relationships and built-in hint types.

doc/examples.md
  Copyable examples for saving-page style, lazy lists, and same-step targets.

doc/troubleshooting.md
  Symptom-based debugging guide.

Before changing code in this component, read CONTRIBUTING.md first.

Example App #

The example app is a scenario playground. The entry stays small in example/lib/main.dart, while each usage pattern lives in its own file:

example/lib/src/scenarios/basic_steps_scenario.dart
example/lib/src/scenarios/pointer_hint_scenario.dart
example/lib/src/scenarios/same_step_hints_scenario.dart
example/lib/src/scenarios/same_step_auto_scroll_scenario.dart
example/lib/src/scenarios/lazy_target_reveal_scenario.dart
example/lib/src/scenarios/dynamic_steps_scenario.dart
example/lib/src/scenarios/side_anchor_scenario.dart
example/lib/src/scenarios/large_group_anchor_scenario.dart
example/lib/src/scenarios/target_decoration_scenario.dart
example/lib/src/scenarios/custom_anchor_scenario.dart
example/lib/src/scenarios/controller_usage_scenario.dart

Run it with:

cd example
flutter run -d ios

Basic Usage #

Basic spotlight guide flow

SpotlightGuidePortal(
  enabled: shouldShowGuide,
  steps: <SpotlightGuideStep>[
    SpotlightGuideStep.item(
      SpotlightGuideStepItem(
        targetId: 'more-button',
        hintBuilder: (BuildContext context, SpotlightGuideStepContext guide) {
          return SpotlightGuideTextHint(
            guide: guide,
            title: 'More actions',
            message: 'Open this menu for advanced options.',
          );
        },
      ),
    ),
  ],
  onFinish: markGuideSeen,
  child: SpotlightGuideTarget(
    id: 'more-button',
    child: IconButton(
      icon: const Icon(Icons.more_horiz),
      onPressed: openMore,
    ),
  ),
)

SpotlightGuideTextHint is the quickest built-in hint. It shows progress, Back/Next/Done actions, scrolls long content inside the bubble, and still uses the same placement, margin, pointer, and decoration system as custom hints.

Pointer Hints #

Built-in tap pointer hint

Use a pointer when the guide should visually point at a small tap target. A pointer is any widget: the built-in tap pointer is only a shortcut, and you can replace it with Image.asset, CustomPaint, an icon badge, or an app-specific animation when needed.

Pointer direction follows the resolved hint placement. Use physical left/right only for fixed layouts, semantic start/end when the side should mirror in RTL, and horizontalAuto or auto when localization or responsive layout can move the target. If a pointer is only decorative and the bubble arrow should still point directly at the target, set anchorMode: SpotlightGuidePointerAnchorMode.target.

pointerAnchorPosition chooses which point inside the pointer widget attaches to the target. targetAnchorPosition then chooses which point inside the pointer the bubble arrow attaches to. For example, center(4) keeps the pointer contact stable and moves the arrow attachment 4 logical pixels from the pointer center. Negative offsets are allowed.

gap is the signed main-axis distance for the active anchor chain. With the default pointer anchor mode, the pointer touches the target side and gap is the distance from the pointer's far edge to the bubble anchor tip. If the bubble uses SpotlightGuideNoAnchor, the hint edge is treated as that tip. Without a pointer, or when the pointer uses anchorMode: SpotlightGuidePointerAnchorMode.target, gap remains the target to bubble distance.

Use SpotlightGuideHintPointer.targetGap when the pointer itself should sit away from the target. Positive values move it away from the target in the resolved placement direction, negative values pull it back toward the target, and zero keeps the pointer touching the target. Custom pointer widgets can omit size to use the child's laid-out size; provide size when an image or animation should reserve a stable visual slot.

Use visualOffset only for small asset-level nudges. It moves the pointer widget's painted content, but does not move the target anchor, pointer layout slot, bubble anchor, or bubble position. Use SpotlightGuidePointerOffset.directional when the horizontal nudge should mirror in RTL.

For directional artwork, keep the pointer child as the default asset and use builder to wrap it with resolved layout information. The builder receives a SpotlightGuidePointerContext; targetDirection tells you where the target is relative to the pointer after auto, start/end, and RTL are resolved. Use rotationToTarget() when the pointer should face the target. Pass from when your asset's unrotated pose does not point up. up() is the default source pose. upRight() describes a northeast-facing asset, and upRight(0) is identical to upRight(). Direction constructors also accept a clockwise offset from that named pose, such as right(math.pi / 2).

The returned rotation is resolved against the current target side, so the same source pose can rotate clockwise on one side and counterclockwise on the opposite side. For example, a right-side pointer and a left-side pointer mirror each other automatically.

SpotlightGuideStepItem(
  targetId: 'more-button',
  placement: SpotlightGuidePlacement.bottom,
  gap: 10,
  decoration: const SpotlightGuideBubbleDecoration(
    anchor: SpotlightGuideNoAnchor(),
  ),
  hintBuilder: (BuildContext context, SpotlightGuideStepContext guide) {
    return SpotlightGuideTextHint(
      guide: guide,
      title: 'Tap here',
      message: 'The pointer can be any widget.',
      pointer: SpotlightGuideHintPointer.tap(
        anchorMode: SpotlightGuidePointerAnchorMode.pointer,
        visualOffset: SpotlightGuidePointerOffset.directional(end: 2),
        builder: (
          BuildContext context,
          SpotlightGuidePointerContext pointer,
          Widget child,
        ) {
          return Transform.rotate(
            angle: pointer.rotationToTarget(),
            child: child,
          );
        },
      ),
    );
  },
)

For example, with import 'dart:math' as math;, an image whose unrotated pose points right and is drawn another 90 degrees clockwise from that right axis can use:

Transform.rotate(
  angle: pointer.rotationToTarget(
    from: SpotlightGuidePointerDirection.right(math.pi / 2),
  ),
  child: child,
)

Multiple Hints In One Step #

Several hints in one step

Use SpotlightGuideStep(items: ...) when several hints should appear together. A single item can also highlight several targets with targetIds.

SpotlightGuideStep(
  items: <SpotlightGuideStepItem>[
    SpotlightGuideStepItem(targetId: 'calendar', hintBuilder: buildCalendarHint),
    SpotlightGuideStepItem(targetId: 'more', hintBuilder: buildMoreHint),
  ],
)

Repeated Target IDs #

Custom anchors and repeated target groups

When several mounted SpotlightGuideTarget widgets use the same id, the component treats them as one logical target group. All instances are highlighted and the default placement anchor is the union of that group. The guide does not pick the first or last mounted instance.

If the group should be highlighted but one instance should be the precise anchor, set anchorId on that SpotlightGuideTarget and point anchorTargetId at it. This keeps the target in the normal registration chain, so insertion, removal, and active hint rebuilds continue to work.

SpotlightGuideTarget(
  id: 'summary-card-group',
  anchorId: 'summary-total-cost-anchor',
  child: totalCostCard,
)

SpotlightGuideStepItem(
  targetId: 'summary-card-group',
  anchorTargetId: 'summary-total-cost-anchor',
  hintBuilder: buildTotalCostHint,
)

For repeated list rows where only one row should be highlighted, use a unique id such as order-row-${order.id} or use targetKey.

Target Decoration #

Use targetDecoration when the spotlight hole needs a specific shape, padding, outer rings or glow. The decoration paints on the overlay only; it does not wrap or modify the real target widget.

Target decoration guide scenarios

Use translucent SpotlightGuideTargetRingLayers for a crisp border-style halo, SpotlightGuideTargetGlowLayer for a blurred soft halo, or SpotlightGuideTargetDashedOutlineLayer for temporary selection and review states. Layers paint in list order and follow the resolved target shape.

SpotlightGuideStep.item(
  SpotlightGuideStepItem(
    targetId: 'price-card',
    targetDecoration: const SpotlightGuideTargetDecoration(
      padding: EdgeInsets.all(8),
      shape: SpotlightGuideRoundedRectTargetShape(
        borderRadius: BorderRadius.all(Radius.circular(18)),
      ),
      layers: <SpotlightGuideTargetLayer>[
        SpotlightGuideTargetRingLayer(color: Color(0x1AFFFFFF), width: 16),
        SpotlightGuideTargetRingLayer(color: Color(0x33FFFFFF), width: 8),
      ],
    ),
    hintBuilder: buildPriceHint,
  ),
)

Built-in shapes include rounded rectangles and ovals. Implement SpotlightGuideTargetShape for custom hole paths, or SpotlightGuideTargetLayer for custom paint effects around the hole.

targetDecoration: const SpotlightGuideTargetDecoration(
  layers: <SpotlightGuideTargetLayer>[
    SpotlightGuideTargetDashedOutlineLayer(
      color: Colors.white,
      width: 3,
      dashLength: 10,
      gapLength: 6,
      outset: 8,
    ),
  ],
)

Dynamic Or API-Driven Steps #

Dynamic guide steps

For automatic guides, rebuild steps after data loads. A portal without an external controller starts when steps first becomes non-empty.

SpotlightGuidePortal(
  steps: guideStepsFromServer,
  child: page,
)

For user-controlled or scenario-triggered guides, keep a controller and call showSteps. steps can be omitted on the portal when all guide content comes from an API or another runtime decision.

final controller = SpotlightGuidePortalController();

SpotlightGuidePortal(
  controller: controller,
  autoStart: false,
  missingTargetBehavior: SpotlightGuideMissingTargetBehavior.skip,
  child: page,
);

final List<SpotlightGuideStep> steps = buildGuideSteps(response);
if (steps.isEmpty) {
  controller.hide();
} else {
  controller.showSteps(steps);
}

When guide steps come from an API, set missingTargetBehavior: SpotlightGuideMissingTargetBehavior.skip on the portal so stale or inapplicable target ids do not leave a blank guide active. If one specific item is expected to appear later, override that item back to wait. Skipped unavailable steps are removed from the active sequence, so controller.total, controller.isLast, and the same values in SpotlightGuideStepContext update as if those steps had been removed from the list.

Use onStateChanged when external state depends on dynamic target availability or guide progress. It fires when a step is shown, hidden, when portal steps change while active, and when targets register or unregister.

SpotlightGuidePortal(
  onStateChanged: (SpotlightGuideStateContext state) {
    debugPrint(
      'guide ${state.index + 1}/${state.total}, '
      'resolved=${state.resolvedItemCount}',
    );
  },
  steps: steps,
  child: page,
)

Scroll And Lazy Targets #

Same-step auto scroll Lazy target reveal
Same-step automatic scroll Lazy target reveal scroll

If a target is already built but outside a scrollable viewport, the default SpotlightGuideRevealOptions calls Scrollable.ensureVisible. It does not scroll when the target is already fully visible. Use scrollPolicy: SpotlightGuideRevealScrollPolicy.always when a step should always realign the target, or visibilityPadding when sticky headers or bottom bars should count as unsafe space. During reveal scrolling, the default presentation keeps the barrier visible and waits until scrolling/layout settles before showing hints and spotlight holes. Use SpotlightGuideLiveRevealPresentationStrategy when an app intentionally wants resolved hints and holes to track animated scrolling.

When one hint highlights a large targetIds group, set anchorTargetId to the main target. If the full group is too large to fit, default reveal prioritizes the anchor, so an already visible anchor will not scroll the page just because the larger highlighted area extends outside the viewport. Override scrollTargetPolicy when a page needs different behavior.

SpotlightGuideRevealOptions(
  scrollTargetPolicy: SpotlightGuideRevealScrollTargetPolicy.anchorTarget,
)

If a target is not built yet, such as a lazy list row, use onReveal to scroll close to it, switch tabs, expand a section, or load data. After the callback completes, the portal waits for layout and then applies the default reveal behavior.

SpotlightGuideStepItem(
  targetId: 'order-row-50',
  onReveal: (SpotlightGuideRevealContext context) async {
    await context.scrollToIndex(
      controller: listController,
      index: 50,
      itemExtent: rowHeight,
      alignment: 0.2,
    );
  },
  revealOptions: const SpotlightGuideRevealOptions(
    alignment: 0.5,
    duration: Duration(milliseconds: 250),
  ),
  hintBuilder: buildHint,
)

For multiple items in the same step, SpotlightGuideStepAutoScrollOptions can briefly scroll to hidden later targets so users understand the whole introduced area. It is enabled by default and starts when a later target is not fully visible, or when a not-yet-built later target provides an onReveal hook. During the default auto-scroll transition, the outgoing hint is hidden before scrolling starts and the next hint appears after the target settles, so hints do not detach from moving or offscreen targets. Use autoScrollOptions.onAutoScrollItemChanged to react when that focused item changes. The callback receives [SpotlightGuideAutoScrollItemContext] with itemIndex, itemTotal, highlightTargetIds, and optional [SpotlightGuideStepItem.key]. Use one item with targetIds when a single hint should light several registered targets at once.

Barrier Style #

Configure the guide background with SpotlightGuideBarrierStyle. Color and blur are clipped to the dimmed area, so highlighted targets keep their original brightness and sharpness. Set it on SpotlightGuidePortal for a shared default; set it on a SpotlightGuideStep to override only the fields that step needs. Visual holes are clipped to the visible overlay before painting, so oversized targets keep their rounded corners inside the screen.

SpotlightGuidePortal(
  barrier: const SpotlightGuideBarrierStyle(
    color: Color(0x99000000),
    blurSigma: 4,
  ),
  steps: <SpotlightGuideStep>[
    SpotlightGuideStep(
      // Keeps the portal blur, only changes the color for this step.
      barrier: const SpotlightGuideBarrierStyle(color: Color(0x66000000)),
      items: items,
    ),
  ],
  child: page,
)

Barrier Taps #

Barrier tap dismiss modes

The dim barrier absorbs taps so they never reach the page behind the guide. By default, tapping empty space does not close or advance the guide.

Use barrierDismissBehavior for common close behavior:

SpotlightGuidePortal(
  barrierDismissBehavior: SpotlightGuideBarrierDismissBehavior.onComplete,
  steps: steps,
  child: page,
)

onComplete only finishes after the last step is visible and any same-step auto-scroll sequence has reached its final item. Use anytime when the product intentionally allows closing mid-flow.

Provide onBarrierTap for custom behavior such as tap-anywhere-to-continue. The callback receives the active controller, so this works even without supplying an external one.

SpotlightGuidePortal(
  onBarrierTap: (controller) => controller.next(),
  steps: steps,
  child: page,
)

Show Once #

The package does not depend on a persistence plugin. Keep the guide lightweight and connect it to your own local or remote state:

SpotlightGuidePortal(
  enabled: !settings.hasSeenHomeGuide,
  steps: homeGuideSteps,
  onFinish: () => settings.markHomeGuideSeen(),
  child: page,
)

Tap The Highlighted Target #

Set allowTargetInteraction: true on an item to let taps inside its spotlight hole pass through to the real widget behind the guide. This is the "tap this button to continue" pattern. The barrier keeps absorbing taps everywhere else. Only the target rect itself passes through; the surrounding padding from targetDecoration.padding stays absorbed so a neighbouring control is not hit by accident.

SpotlightGuideStep.item(
  SpotlightGuideStepItem(
    targetId: 'send-button',
    allowTargetInteraction: true,
    hintBuilder: (context, guide) => const Text('Tap send to continue'),
  ),
)

Stepping Back And Jumping #

Controller API guide

The controller can start the portal-owned sequence with showPortal() or a runtime sequence with showSteps(steps). The controller and the guide context inside a hintBuilder can both navigate or close the active guide with next(), previous(), goTo(index), reset(), hide(), and finish(). previous() is a no-op on the first step, and goTo clamps the index. hide() closes without onFinish; finish() closes and reports completion.

controller.showPortal();
controller.showSteps(steps);
controller.previous();
controller.goTo(2);
controller.hide();

RTL And Anchors #

Horizontal auto placement choosing left and right arrows

Use SpotlightGuideAnchorPosition.start, center, or end for semantic alignment. start and end follow Directionality, so Arabic and other RTL layouts mirror correctly.

Custom UI #

Custom anchors and repeated target groups

hintBuilder may return:

  • SpotlightGuideBubbleHint for a common bubble plus optional pointer image.
  • SpotlightGuideBubble for a connected bubble arrow without pointer.
  • Any custom widget, image composition, or app-specific guide UI.

The layout data is available through SpotlightGuideStepContext. Use guide.indicatorDirection when a custom hint needs to know which physical side the anchor is on. For example, a hint placed below a target has an up indicator because the anchor sits on the bubble's top edge and points toward the target. Path-based custom anchors can read the same resolved side from SpotlightGuideAnchorPathBuilder.direction inside addToPath. Use guide.targetAnchorPosition when custom pointer or bubble layouts need to resolve the same anchor position that built-in hints use.

For the full API and built-in hint catalog, see doc/reference.md.

0
likes
0
points
33
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter overlay guide for spotlighting widgets with dynamic targets, reveal scrolling, custom hints, and robust widget tests.

Repository (GitHub)
View/report issues

Topics

#spotlight #onboarding #tutorial #overlay #flutter

License

unknown (license)

Dependencies

flutter

More

Packages that depend on spotlight_guide