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.
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();
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) */ };
TourHost.StepChanged += (s, e) =>
{
Console.WriteLine($"Step {e.StepIndex + 1}/{e.TotalSteps}: {e.Step.Title}");
};
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:
- Check parent elements for explicit
FlowDirection - 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.
Built with precision by MarketAlly
Enterprise-grade onboarding solutions for .NET MAUI applications.