cfn_widget_spotlight

Customizable spotlight overlays and guided tours for Flutter. The package can highlight one widget, multiple widgets at the same time, or a sequence of single/multi-target steps.

Demo

Widget Spotlight demo

Features

  • Sharp cutouts over a dimmed, blurred backdrop
  • Rectangle, rounded rectangle, circle, and oval highlights
  • Automatic or explicit card placement on any side of a target
  • Single-target and simultaneous multi-target overlays
  • Multi-step tours with progress, next, back, done, and skip controls
  • Completely custom card widgets through contentBuilder
  • Configurable colors, typography, spacing, elevation, pointer, and animation
  • Optional interaction with the highlighted widget
  • Barrier dismissal, completion results, callbacks, and controller API
  • Accessibility route and target semantics

Installation

Add the package from pub.dev:

flutter pub add cfn_widget_spotlight

Or add it manually:

dependencies:
  cfn_widget_spotlight: ^0.0.3

Then import it:

import 'package:cfn_widget_spotlight/cfn_widget_spotlight.dart';

Single spotlight

Give the widget being highlighted a GlobalKey, then show the overlay after the widget is mounted.

final editProfileKey = GlobalKey();

ElevatedButton(
  key: editProfileKey,
  onPressed: editProfile,
  child: const Text('Edit Profile'),
);

await CfnWidgetSpotlight.show(
  context,
  target: SpotlightTarget(
    key: editProfileKey,
    title: 'Personalize Your Profile',
    description:
        'Update your profile details and choose what to share.',
    placement: SpotlightPlacement.below,
    nextLabel: 'Continue to Publish',
  ),
);

Multiple simultaneous overlays

One step can contain several cutouts and cards. Set showNavigation: false for informational cards so only the primary card shows controls.

await CfnWidgetSpotlight.showMultiple(
  context,
  targets: [
    SpotlightTarget(
      key: announcementsTabKey,
      title: 'Announcement',
      description: 'Stay informed and share updates.',
      showNavigation: false,
      placement: SpotlightPlacement.below,
    ),
    SpotlightTarget(
      key: announcementButtonKey,
      title: 'Keep Members Updated',
      description: 'Share updates, events, insights, and important notices.',
      placement: SpotlightPlacement.above,
    ),
  ],
);

Multi-step tour

Each SpotlightStep may contain one or many targets.

final controller = SpotlightController();

final result = await CfnWidgetSpotlight.showTour(
  context,
  controller: controller,
  steps: [
    SpotlightStep.single(
      SpotlightTarget(
        key: profileKey,
        title: 'Your profile',
        description: 'Introduce yourself to the community.',
      ),
    ),
    SpotlightStep(
      targets: [
        SpotlightTarget(
          key: bioKey,
          title: 'Bio',
          showNavigation: false,
        ),
        SpotlightTarget(
          key: linksKey,
          title: 'Links',
          description: 'Add your creative presence.',
        ),
      ],
    ),
  ],
  onStepChanged: (index) => debugPrint('Showing step $index'),
);

if (result.completed) {
  // Persist that onboarding has been completed.
}

The controller also supports next(), previous(), goTo(index), skip(), and dismiss().

Custom content

contentBuilder replaces the default card. Its details provide the controller, tour progress, target progress, and measured target rectangle.

SpotlightTarget(
  key: targetKey,
  placement: SpotlightPlacement.auto,
  shape: SpotlightShape.circle,
  contentBuilder: (context, details) {
    return Card(
      color: Colors.indigo,
      child: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            const Text('Your custom content'),
            FilledButton(
              onPressed: details.controller.next,
              child: const Text('Continue'),
            ),
          ],
        ),
      ),
    );
  },
);

Styling

Pass SpotlightThemeData to any show method. copyWith is available when only a few defaults need changing.

final spotlightTheme = const SpotlightThemeData().copyWith(
  barrierColor: Colors.black.withValues(alpha: 0.62),
  blurSigma: 4,
  primaryColor: Colors.deepPurple,
  highlightBorderColor: Colors.white,
  cardBorderRadius: BorderRadius.circular(28),
  cardBorderSide: const BorderSide(color: Colors.white, width: 1.5),
  animationDuration: const Duration(milliseconds: 300),
);

await CfnWidgetSpotlight.show(
  context,
  target: target,
  theme: spotlightTheme,
  barrierDismissible: true,
);

Per-target customization includes placement, alignment, shape, padding, border radius, card gap/offset, maximum width, pointer visibility, labels, semantics, and target interaction.

Per-target card and button styling

Customize the default card without rebuilding its content:

SpotlightTarget(
  key: targetKey,
  title: 'Custom appearance',
  description: 'The standard content with your own visual design.',
  cardColor: const Color(0xFF171B2E),
  cardBorderRadius: BorderRadius.circular(32),
  cardBorderSide: const BorderSide(color: Colors.orange, width: 2),
  cardPadding: const EdgeInsets.all(24),
  cardElevation: 18,
  titleStyle: const TextStyle(
    color: Colors.white,
    fontSize: 20,
    fontWeight: FontWeight.bold,
  ),
  descriptionStyle: const TextStyle(color: Colors.white70),
  primaryButtonStyle: FilledButton.styleFrom(
    backgroundColor: Colors.orange,
    foregroundColor: Colors.black,
    padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(14),
    ),
  ),
);

Use navigationBuilder when the progress label and buttons need completely custom widgets:

SpotlightTarget(
  key: targetKey,
  title: 'Custom navigation',
  navigationBuilder: (context, navigation) {
    return Row(
      children: [
        Text(navigation.progressLabel),
        const Spacer(),
        if (navigation.canGoBack)
          IconButton(
            onPressed: navigation.back,
            icon: const Icon(Icons.chevron_left),
          ),
        IconButton(
          onPressed: navigation.next,
          icon: const Icon(Icons.chevron_right),
        ),
      ],
    );
  },
);

Set primaryButtonStyle and secondaryButtonStyle on SpotlightThemeData to apply button styling to the entire tour. Use contentBuilder when the complete card layout should be replaced.

Rich content while the keyboard is open

Cards avoid the software keyboard by default. Use bodyBuilder for rich rows while keeping the standard card, progress, and navigation controls:

SpotlightTarget(
  key: sentimentKey,
  title: "What's your outlook?",
  placement: SpotlightPlacement.below,
  cardBorderRadius: BorderRadius.circular(24),
  bodyBuilder: (context, details) => const Column(
    mainAxisSize: MainAxisSize.min,
    children: [
      ListTile(
        dense: true,
        contentPadding: EdgeInsets.zero,
        leading: CircleAvatar(
          backgroundColor: Colors.green,
          child: Icon(Icons.trending_up, color: Colors.white),
        ),
        title: Text('Bullish'),
        subtitle: Text('Price will go up'),
      ),
      ListTile(
        dense: true,
        contentPadding: EdgeInsets.zero,
        leading: CircleAvatar(
          backgroundColor: Colors.red,
          child: Icon(Icons.trending_down, color: Colors.white),
        ),
        title: Text('Bearish'),
        subtitle: Text('Price will go down'),
      ),
      ListTile(
        dense: true,
        contentPadding: EdgeInsets.zero,
        leading: CircleAvatar(
          backgroundColor: Colors.orange,
          child: Icon(Icons.horizontal_rule, color: Colors.white),
        ),
        title: Text('Neutral'),
        subtitle: Text('Price will stay within a range'),
      ),
    ],
  ),
);

Set avoidKeyboard: false on SpotlightThemeData only when guide cards are intentionally allowed behind the keyboard.

Target interaction

Set allowTargetInteraction: true to let taps inside the cutout reach the original widget. Alternatively, set onTargetTap to handle that region in the overlay itself.

The GlobalKey target must be mounted and laid out when its step is displayed. For targets inside a scroll view, scroll the target into view before advancing to its step.

Libraries

cfn_widget_spotlight
Customizable spotlight overlays and guided tours for Flutter.