Files
maspotlighttour/README.md

25 KiB

MASpotlightTour

A powerful, declarative spotlight tour and onboarding library for .NET MAUI applications. Create beautiful feature tours with spotlight overlays, callout cards, and step-by-step user guidance using simple XAML attached properties.

NuGet Version NuGet Downloads License: MIT .NET Platform

Features

  • Declarative XAML Syntax - Tag UI elements with attached properties; no code-behind required for basic tours
  • Multiple Display Modes - Spotlight with callout, callout-only (no dimming), or spotlight with inline labels
  • Flexible Callout Positioning - Following (relative to element), fixed corner, or auto-corner placement
  • Customizable Spotlights - Rectangle, rounded rectangle, or circle shapes with configurable padding
  • Corner Navigator - Compact navigation control with step indicators and Previous/Next/Skip buttons
  • Animation Effects - Entrance/exit animations, step transitions, and spotlight effects with presets (Subtle, Playful, Elegant, Snappy)
  • Intro Views - Show custom welcome screens before the tour begins
  • Auto-Advance - Automatically progress through steps on a timer
  • Tour Looping - Repeat tours automatically for kiosk/demo modes
  • Theme Support - Light, Dark, and System theme modes with automatic switching
  • RTL Support - Full right-to-left layout support for Arabic, Hebrew, and other RTL languages
  • Awaitable Tours - Chain actions after tour completion using async/await
  • Tour Groups - Organize multiple tours on the same page
  • Localization - Built-in support for 8 languages (EN, ES, FR, DE, ZH, JA, PT, IT) with easy extensibility
  • Cross-Platform - iOS, Android, Windows, and macOS

Installation

dotnet add package MarketAlly.SpotlightTour.Maui

Or via the NuGet Package Manager:

Install-Package MarketAlly.SpotlightTour.Maui

Quick Start

1. Add the namespace to your XAML

xmlns:tour="clr-namespace:MarketAlly.SpotlightTour.Maui;assembly=MarketAlly.SpotlightTour.Maui"

2. Tag elements you want to highlight

<Button
    Text="Click Me"
    tour:Onboarding.StepKey="welcome"
    tour:Onboarding.Title="Welcome!"
    tour:Onboarding.Description="This button does something amazing."
    tour:Onboarding.Order="1" />

<Entry
    Placeholder="Enter your name"
    tour:Onboarding.StepKey="name-input"
    tour:Onboarding.Title="Your Name"
    tour:Onboarding.Description="Type your name here to get started."
    tour:Onboarding.Order="2" />

3. Add the OnboardingHost overlay

<Grid>
    <!-- Your page content -->
    <VerticalStackLayout>
        <!-- ... your UI elements ... -->
    </VerticalStackLayout>

    <!-- Add at the end, as an overlay -->
    <tour:OnboardingHost x:Name="TourHost" />
</Grid>

4. Start the tour

// Start the tour (scans for tagged elements automatically)
await TourHost.StartTourAsync(this.Content);

Display Modes

SpotlightWithCallout (Default)

Full experience with dimmed overlay, spotlight cutout, and callout card with navigation.

<tour:OnboardingHost DisplayMode="SpotlightWithCallout" />

CalloutOnly

Floating callout cards without dimming - ideal for light-touch guidance that doesn't block interaction.

<tour:OnboardingHost DisplayMode="CalloutOnly" />

SpotlightWithInlineLabel

Dimmed overlay with spotlight, but uses compact inline labels instead of callout cards. Best used with the corner navigator.

<tour:OnboardingHost
    DisplayMode="SpotlightWithInlineLabel"
    ShowCornerNavigator="True" />

Callout Positioning

Following Mode (Default)

Callout positions relative to the highlighted element (Top, Bottom, Left, Right, or Auto).

<tour:OnboardingHost CalloutPositionMode="Following" />

<!-- Per-element placement override -->
<Button tour:Onboarding.Placement="Top" ... />

Fixed Corner Mode

Callout stays in a fixed screen corner while spotlights move.

<tour:OnboardingHost
    CalloutPositionMode="FixedCorner"
    CalloutCorner="BottomLeft" />

Auto Corner Mode

Callout automatically positions in the corner that least interferes with the highlighted element.

<tour:OnboardingHost CalloutPositionMode="AutoCorner" />

Corner Navigator

A compact navigation control that sits in a screen corner with step indicators and navigation buttons.

<tour:OnboardingHost
    ShowCornerNavigator="True"
    CornerNavigatorPlacement="BottomRight"
    ShowSkipButton="True" />

When set to Auto, the navigator automatically positions to avoid overlapping with the spotlight and callout card.

<tour:OnboardingHost
    ShowCornerNavigator="True"
    CornerNavigatorPlacement="Auto"
    CalloutPositionMode="AutoCorner" />

Intro Views

Show a custom welcome screen before the tour begins:

<tour:OnboardingHost x:Name="TourHost">
    <tour:OnboardingHost.IntroView>
        <Frame BackgroundColor="White" Padding="30" CornerRadius="16">
            <VerticalStackLayout Spacing="20">
                <Label Text="Welcome!" FontSize="28" FontAttributes="Bold" />
                <Label Text="Let us show you around." />
                <Button Text="Start Tour" Clicked="OnStartTourClicked" />
            </VerticalStackLayout>
        </Frame>
    </tour:OnboardingHost.IntroView>
</tour:OnboardingHost>
private async void OnStartTourClicked(object sender, EventArgs e)
{
    TourHost.DismissIntro(); // Dismisses intro and continues to tour
}

// Start with intro
await TourHost.StartTourWithIntroAsync(this.Content);

Spotlight Customization

Shapes

<!-- Rounded rectangle (default) -->
<Button tour:Onboarding.SpotlightShape="RoundedRectangle"
        tour:Onboarding.SpotlightCornerRadius="12" ... />

<!-- Circle -->
<Image tour:Onboarding.SpotlightShape="Circle" ... />

<!-- Rectangle -->
<Frame tour:Onboarding.SpotlightShape="Rectangle" ... />

Padding

<!-- Uniform padding -->
<Button tour:Onboarding.SpotlightPadding="16" ... />

<!-- Per-side padding -->
<Button tour:Onboarding.SpotlightPadding="8,16,8,16" ... />

Tap Behavior

<!-- Tap spotlight to advance -->
<Button tour:Onboarding.TapBehavior="Advance" ... />

<!-- Tap spotlight to close tour -->
<Button tour:Onboarding.TapBehavior="Close" ... />

<!-- Allow interaction with underlying element -->
<Button tour:Onboarding.TapBehavior="AllowInteraction" ... />

Theming

<!-- Light theme -->
<tour:OnboardingHost Theme="Light" />

<!-- Dark theme -->
<tour:OnboardingHost Theme="Dark" />

<!-- Follow system theme -->
<tour:OnboardingHost Theme="System" />

Overlay Customization

<tour:OnboardingHost
    DimOpacity="0.7"
    DimColor="Black" />

Tour Groups

Organize multiple tours on the same page:

<!-- Basic features tour -->
<Button tour:Onboarding.Group="basics" tour:Onboarding.Order="1" ... />
<Entry tour:Onboarding.Group="basics" tour:Onboarding.Order="2" ... />

<!-- Advanced features tour -->
<Button tour:Onboarding.Group="advanced" tour:Onboarding.Order="1" ... />
<Slider tour:Onboarding.Group="advanced" tour:Onboarding.Order="2" ... />
// Start specific group
await TourHost.StartTourAsync(this.Content, group: "basics");

Auto-Advance & Looping

Auto-Advance

Automatically progress through steps:

<tour:OnboardingHost AutoAdvanceDelay="3000" /> <!-- 3 seconds per step -->

Auto-Loop

Repeat the tour automatically (great for kiosk/demo modes):

<tour:OnboardingHost
    AutoAdvanceDelay="2000"
    AutoLoop="3" /> <!-- Repeat 3 times -->

Auto-Start

Start the tour automatically when the page loads:

<tour:OnboardingHost AutoStartDelay="500" /> <!-- Start after 500ms -->

Awaitable Tours

Chain actions after tour completion:

var result = await TourHost.StartTourAsync(this.Content);

switch (result)
{
    case TourResult.Completed:
        await DisplayAlert("Done!", "You completed the tour.", "OK");
        break;
    case TourResult.Skipped:
        // User skipped the tour
        break;
    case TourResult.Cancelled:
        // Tour was cancelled programmatically
        break;
    case TourResult.NoSteps:
        // No tour steps were found
        break;
}

Programmatic Navigation

// Navigate to specific step
await TourHost.GoToStepAsync(2);

// Navigate by step key
var step = OnboardingScanner.FindStepByKey(this.Content, "welcome");

// Go to next/previous (async methods preferred)
await TourHost.GoToNextStepAsync();
await TourHost.GoToPreviousStepAsync();

// End tour
await TourHost.CompleteTourAsync();
TourHost.SkipTour();
TourHost.StopTour();

Step Actions

Execute custom code when entering or leaving tour steps. Perfect for UI preparation like switching tabs, opening drawers, or triggering animations before the spotlight appears.

Attached Property (Per-Element)

// In code-behind, attach action directly to the element
Onboarding.SetOnEntering(SettingsTab, async (step, cancellationToken) =>
{
    // Switch to the settings tab before spotlight appears
    MainTabView.SelectedIndex = 2;
    await Task.Delay(100); // Wait for tab transition
});

Onboarding.SetOnLeaving(SettingsTab, async (step, cancellationToken) =>
{
    // Cleanup when leaving this step
    MainTabView.SelectedIndex = 0;
});

Centralized Registration (By StepKey)

// Register actions by step key - keeps tour logic organized
TourHost.RegisterStepEnteringAction("drawer-step", async (step, token) =>
{
    Shell.Current.FlyoutIsPresented = true;
    await Task.Delay(250); // Wait for drawer animation
});

TourHost.RegisterStepEnteringAction("settings-tab", async (step, token) =>
{
    SettingsTabButton.IsSelected = true;
});

// Remove registered actions when no longer needed
TourHost.UnregisterStepEnteringAction("drawer-step");
TourHost.ClearStepActions(); // Clear all

Step Action Events

// Global event - fires for every step (useful for analytics, logging, conditional skipping)
TourHost.StepEntering += async (sender, e) =>
{
    // Skip premium features for non-premium users
    if (e.Step.StepKey == "premium-feature" && !currentUser.IsPremium)
    {
        e.Skip = true; // Skip this step, move to next
        return;
    }

    // Log analytics
    Analytics.Track("tour_step_entering", e.Step.StepKey);
};

// Fires after step is fully visible (animations complete)
TourHost.StepEntered += (sender, e) =>
{
    Console.WriteLine($"Now viewing step: {e.Step.Title}");
};

// Fires before leaving current step
TourHost.StepLeaving += async (sender, e) =>
{
    // Perform cleanup before navigating away
    await SaveStepProgressAsync(e.StepIndex);
};

Execution Order

Actions execute in this order when navigating to a step:

  1. StepLeaving event + actions (previous step)
  2. StepEntering event (can set Skip = true)
  3. OnEntering attached property action
  4. Registered entering action (by StepKey)
  5. Scroll target into view & animate spotlight
  6. StepEntered event

Events

// Tour lifecycle events
TourHost.TourStarted += (s, e) => { /* Tour began */ };
TourHost.TourCompleted += (s, e) => { /* User finished all steps */ };
TourHost.TourSkipped += (s, e) => { /* User skipped */ };
TourHost.TourEnded += (s, e) => { /* Tour ended (any reason) */ };

// Step navigation events
TourHost.StepEntering += async (s, e) => { /* Before entering step - can set e.Skip = true */ };
TourHost.StepEntered += (s, e) => { /* After step is visible */ };
TourHost.StepLeaving += async (s, e) => { /* Before leaving step */ };
TourHost.StepChanged += (s, e) =>
{
    Console.WriteLine($"Step {e.StepIndex + 1}/{e.TotalSteps}: {e.Step.Title}");
};

// Intro events
TourHost.IntroShown += (s, e) => { /* Intro view displayed */ };
TourHost.IntroDismissed += (s, e) => { /* Intro view dismissed */ };

Animation Effects

The library provides a comprehensive animation system with entrance/exit animations, step transitions, and spotlight effects.

Animation Configuration

Configure animations using the AnimationConfiguration class:

using MarketAlly.SpotlightTour.Maui.Animations;

// Use a preset
var playful = AnimationConfiguration.Playful;
var elegant = AnimationConfiguration.Elegant;
var snappy = AnimationConfiguration.Snappy;
var subtle = AnimationConfiguration.Subtle;

// Or customize your own
var custom = new AnimationConfiguration
{
    // Entrance animations
    CalloutEntrance = EntranceAnimation.SlideUpFade,
    OverlayEntrance = EntranceAnimation.FadeIn,
    NavigatorEntrance = EntranceAnimation.SlideFromRight,
    InlineLabelEntrance = EntranceAnimation.PopIn,

    // Exit animations
    CalloutExit = ExitAnimation.FadeOut,
    OverlayExit = ExitAnimation.FadeOut,

    // Step transitions
    StepTransition = StepTransition.Slide,

    // Spotlight effects (continuous animations)
    SpotlightEffect = SpotlightEffect.Pulse,

    // Timing
    EntranceDurationMs = 300,
    ExitDurationMs = 250,
    StepTransitionDurationMs = 200,

    // Easing
    Easing = AnimationEasing.CubicInOut
};

Using Animations with OnboardingHost

<tour:OnboardingHost
    x:Name="TourHost"
    AnimationsEnabled="True"
    AnimationDuration="300" />
// Apply animation configuration
TourHost.AnimationConfiguration = AnimationConfiguration.Playful;

// Or use custom configuration
TourHost.AnimationConfiguration = new AnimationConfiguration
{
    SpotlightEffect = SpotlightEffect.Breathe,
    CalloutEntrance = EntranceAnimation.BounceIn,
    StepTransition = StepTransition.Push
};

Available Animation Types

Entrance Animations

Animation Description
None No animation
FadeIn Fade in from transparent
SlideFromLeft Slide in from left
SlideFromRight Slide in from right
SlideFromTop Slide in from top
SlideFromBottom Slide in from bottom
ScaleUp Scale up from small
BounceIn Bounce in with overshoot
PopIn Pop in with scale
SlideUpFade Slide up while fading in
ZoomIn Zoom in from small

Exit Animations

Animation Description
None No animation
FadeOut Fade out to transparent
SlideToLeft Slide out to left
SlideToRight Slide out to right
SlideToTop Slide out to top
SlideToBottom Slide out to bottom
ScaleDown Scale down to small
ZoomOut Zoom out
PopOut Pop out with scale

Spotlight Effects (Continuous)

Effect Description
None No effect
Pulse Gentle pulsing scale
Breathe Slow breathing scale
Glow Opacity breathing effect

Step Transitions

Transition Description
Crossfade Fade between steps
Slide Slide in direction of navigation
Push Push old content out

Animation Presets

// Subtle - minimal, professional animations
AnimationConfiguration.Subtle

// Playful - bouncy, fun animations
AnimationConfiguration.Playful

// Elegant - smooth, refined animations
AnimationConfiguration.Elegant

// Snappy - fast, responsive animations
AnimationConfiguration.Snappy

// None - no animations
AnimationConfiguration.None

Disable Animations

<!-- Disable all animations -->
<tour:OnboardingHost AnimationsEnabled="False" />

Legacy Animation Duration

For simple animation timing control:

<!-- Custom animation duration (milliseconds) -->
<tour:OnboardingHost AnimationDuration="400" />

Configuration

The library provides a TourConfiguration class for fine-grained control over timing, sizing, and visual settings:

// Create custom configuration
var config = new TourConfiguration
{
    // Timing
    ScrollTimeoutMs = 1500,           // Timeout for scroll operations
    LayoutSettleDelayMs = 100,        // Delay after scrolling
    DefaultAnimationDurationMs = 300, // Animation duration

    // Sizing
    CalloutMaxWidth = 450,            // Maximum callout width
    NavigatorEstimatedWidth = 220,    // Navigator width for auto-placement
    DefaultMargin = 16,               // Default margin from edges

    // Visual
    DisabledButtonOpacity = 0.4,      // Opacity for disabled buttons
    DefaultDimOpacity = 0.7,          // Default overlay opacity
    CalloutCornerRadius = 16,         // Callout card corner radius
    NavButtonSize = 40,               // Navigation button size

    // Colors
    PrimaryAccentColor = "#0078D4",   // Primary button color
    SuccessColor = "#107C10",         // Done button color
    DangerColor = "#D13438",          // Skip button color
};

// Use configuration with components
var overlay = new SpotlightOverlay(config);
var callout = new CalloutCard(config);

RTL (Right-to-Left) Support

The library includes full RTL support for languages like Arabic, Hebrew, and Persian:

<!-- Auto-detect from system culture or parent -->
<onboard:OnboardingHost TourFlowDirection="MatchParent" />

<!-- Force RTL layout -->
<onboard:OnboardingHost TourFlowDirection="RightToLeft" />

<!-- Force LTR layout -->
<onboard:OnboardingHost TourFlowDirection="LeftToRight" />

When MatchParent is set (the default), the library will:

  1. Check parent elements for explicit FlowDirection
  2. Fall back to system culture (CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft)

RTL mode automatically:

  • Reverses button order in callout cards and navigator
  • Adjusts text alignment for titles and descriptions
  • Mirrors the overall layout direction

Interfaces for Testability

The library provides interfaces for dependency injection and unit testing:

// IStepScanner - for scanning visual trees
public interface IStepScanner
{
    IReadOnlyList<OnboardingStep> FindSteps(Element root, string? group = null);
    OnboardingStep? FindStepByKey(Element root, string stepKey);
    IReadOnlyList<string> GetGroups(Element root);
    int CountSteps(Element root, string? group = null);
}

// Use the default implementation
IStepScanner scanner = DefaultStepScanner.Instance;
var steps = scanner.FindSteps(this.Content);

Resource Management

OnboardingHost implements IDisposable for proper resource cleanup:

// In a page with tour
public partial class MyPage : ContentPage, IDisposable
{
    private readonly OnboardingHost _tourHost;

    public MyPage()
    {
        InitializeComponent();
        _tourHost = new OnboardingHost();
        // ... setup
    }

    public void Dispose()
    {
        _tourHost.Dispose();
    }
}

Localization

The library includes built-in support for 8 languages with automatic detection based on the device culture:

Supported Languages:

  • English (en)
  • Spanish (es)
  • French (fr)
  • German (de)
  • Chinese (zh)
  • Japanese (ja)
  • Portuguese (pt)
  • Italian (it)

Automatic Language Detection

By default, the library uses CultureInfo.CurrentUICulture to select the appropriate language:

// UI text automatically uses device language
await TourHost.StartTourAsync(this.Content);

Manual Language Selection

You can override the language programmatically:

using MarketAlly.SpotlightTour.Maui.Localization;
using System.Globalization;

// Set to Spanish
TourStrings.CurrentCulture = new CultureInfo("es");

// Set to Japanese
TourStrings.CurrentCulture = new CultureInfo("ja");

// Reset to system default
TourStrings.CurrentCulture = null;

Custom Translations

Add or override translations for any language:

// Add custom translations
TourStrings.RegisterTranslations("es", new Dictionary<string, string>
{
    ["Next"] = "Siguiente paso",
    ["Done"] = "Terminado"
});

// Add a new language
TourStrings.RegisterTranslations("ko", new Dictionary<string, string>
{
    ["Previous"] = "이전",
    ["Next"] = "다음",
    ["Done"] = "완료",
    ["Skip"] = "건너뛰기",
    ["StepIndicator"] = "{0} / {1}"
});

Available String Keys

Key English Description
Previous Previous Back button text
Next Next Forward button text
Done Done Completion button text
Skip Skip Skip tour button text
Close Close Close button text
Start Start Start tour button text
GotIt Got it Acknowledgment button text
Continue Continue Continue button text
Finish Finish Finish button text
StepIndicator {0} / {1} Step counter format

Responding to Language Changes

Components automatically update when the culture changes:

// Subscribe to culture changes
TourStrings.CultureChanged += (s, e) =>
{
    // Handle language change if needed
    Console.WriteLine($"Language changed to: {TourStrings.EffectiveCulture.Name}");
};

Async Best Practices

The library provides proper async methods with cancellation token support:

// Cancellation support
using var cts = new CancellationTokenSource();
cts.CancelAfter(TimeSpan.FromMinutes(5));

var result = await TourHost.StartTourAsync(this.Content, cancellationToken: cts.Token);

// Proper async navigation
await TourHost.GoToNextStepAsync();
await TourHost.GoToPreviousStepAsync();
await TourHost.CompleteTourAsync();
await TourHost.DismissIntroAsync();

// Legacy sync methods are available but marked obsolete
// TourHost.GoToNextStep();  // Use GoToNextStepAsync() instead

Complete Example

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:tour="clr-namespace:MarketAlly.SpotlightTour.Maui;assembly=MarketAlly.SpotlightTour.Maui"
             x:Class="MyApp.MainPage">

    <Grid>
        <ScrollView>
            <VerticalStackLayout Padding="20" Spacing="20">

                <Image Source="logo.png"
                       tour:Onboarding.StepKey="logo"
                       tour:Onboarding.Title="Welcome to MyApp"
                       tour:Onboarding.Description="This is our beautiful logo!"
                       tour:Onboarding.Order="1"
                       tour:Onboarding.SpotlightShape="Circle" />

                <Entry Placeholder="Username"
                       tour:Onboarding.StepKey="username"
                       tour:Onboarding.Title="Enter Username"
                       tour:Onboarding.Description="Type your username to sign in."
                       tour:Onboarding.Order="2" />

                <Entry Placeholder="Password" IsPassword="True"
                       tour:Onboarding.StepKey="password"
                       tour:Onboarding.Title="Enter Password"
                       tour:Onboarding.Description="Your password is secure with us."
                       tour:Onboarding.Order="3" />

                <Button Text="Sign In"
                        tour:Onboarding.StepKey="signin"
                        tour:Onboarding.Title="Sign In"
                        tour:Onboarding.Description="Tap here to access your account."
                        tour:Onboarding.Order="4"
                        Clicked="OnSignInClicked" />

            </VerticalStackLayout>
        </ScrollView>

        <tour:OnboardingHost
            x:Name="TourHost"
            Theme="System"
            ShowCornerNavigator="True"
            CornerNavigatorPlacement="Auto"
            CalloutPositionMode="AutoCorner" />
    </Grid>

</ContentPage>
public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
    }

    protected override async void OnAppearing()
    {
        base.OnAppearing();

        // Check if user has seen the tour
        if (!Preferences.Get("HasSeenTour", false))
        {
            var result = await TourHost.StartTourAsync(this.Content);
            if (result == TourResult.Completed)
            {
                Preferences.Set("HasSeenTour", true);
            }
        }
    }
}

API Reference

For complete API documentation, see API_Reference.md.

Platform Support

Platform Minimum Version
iOS 15.0
Android API 21 (5.0)
Windows 10.0.17763.0
macOS 15.0 (Catalyst)

License

This project is licensed under the MIT License - see the LICENSE file for details.

Repository

https://git.marketally.com/marketally/MASpotlightTour


Built with precision by MarketAlly

Enterprise-grade onboarding solutions for .NET MAUI applications.