CodeWF.AvaloniaControls adds a new Guide control for onboarding, feature walkthroughs, and targeted tooltips in Avalonia desktop applications.
It draws inspiration from AtomUI's Tour concept but focuses on desktop-specific scenarios: menus, submenus, popups, TabItem, delayed target appearance, and window resize — all must be reliably positioned.
First, see it in action within Vex. The guide appears automatically on first launch and can be reopened later via the Help menu:

The control library demo also includes two smaller examples: a basic multi-step guide with cover content, custom buttons, and a non-modal hint with text progress.


What Does Guide Solve?
Highlighting a regular page button is easy; the real challenge is dynamic entry points in desktop applications.
Guide currently covers these scenarios:
- Multi-step guide: previous, next, finish, close.
- Each step can target a different control, or have no target for center-aligned explanations.
- Mask cutout, highlight rounded corners, target margins, and card direction control.
- Auto-scroll targets into view when inside a scrollable region.
- Retry with a delay if the target appears later.
- Locate targets inside
Menu,Popup,Flyout, and other popup layers. - Execute commands or events before entering a step—useful for expanding menus or switching tabs.
- Refresh highlight position after layout changes or window resize.
Source location:
https://github.com/dotnet9/CodeWF.AvaloniaControls/tree/main/src/CodeWF.AvaloniaControls/Controls/Guide
https://github.com/dotnet9/CodeWF.AvaloniaControls/tree/main/src/CodeWF.AvaloniaControls.Themes/Themes/Controls/Guide.axaml
Control Structure
A small set of core types:
Guide: The main control that manages opening, closing, current step, popup, and target resolution.GuideStep: Declarative step in XAML.GuideStepOption/IGuideStepOption: Used when creating steps in code.GuideOverlay: Draws the mask and highlight hole.DefaultGuideIndicator/TextGuideIndicator: Dot or text progress.GuidePlacementMode: Card position.GuideMissingTargetBehavior: Centers, skips, or closes when target is missing.
The template contains three key popups:
PART_MaskPopup: Full-window mask.PART_TargetMaskPopup: Used when the target is inside another popup host.PART_Popup: The guide card.
This structure means the business side only declares "which controls to guide"; the mask, positioning, buttons, indicator, and cleanup are handled internally.
Basic Usage
Place the Guide control in the root layout of your page and assign each GuideStep its target control:
<Grid>
<StackPanel Orientation="Horizontal" Spacing="10">
<Button x:Name="UploadButton" Content="Upload File" />
<Button x:Name="SaveButton" Content="Save Changes" />
<Button x:Name="MoreButton" Content="More Actions" />
</StackPanel>
<codewf:Guide x:Name="BasicGuide" Placement="Bottom" PopupOffset="14">
<codewf:GuideStep
Target="{Binding ElementName=UploadButton}"
Title="Upload File"
Description="Add local files to the processing queue." />
<codewf:GuideStep
Target="{Binding ElementName=SaveButton}"
Placement="Right"
Title="Save Changes"
Description="Save the current workspace." />
<codewf:GuideStep
Target="{Binding ElementName=MoreButton}"
Placement="Top"
Title="More Actions"
Description="Continue to expand export, copy, or batch processing." />
</codewf:Guide>
</Grid>
Open the guide:
BasicGuide.GoTo(0);
BasicGuide.Show();
For a non-modal hint, simply hide the mask; you can also switch to a text indicator:
<codewf:Guide
x:Name="NonMaskGuide"
IsShowMask="False"
Placement="Top"
StyleType="Primary">
<codewf:Guide.Indicator>
<codewf:TextGuideIndicator />
</codewf:Guide.Indicator>
</codewf:Guide>
Individual steps can also adjust the highlight area:
<codewf:GuideStep
Target="{Binding ElementName=PreviewPanel}"
Placement="Left"
GapOffsetX="16"
GapOffsetY="16"
GapRadius="14"
Title="Custom Highlight Area"
Description="Widen the selection margins and rounded corners to highlight the entire block." />
Mask and Positioning
GuideOverlay creates a hole using the EvenOdd geometry rule: first draw a full-screen rectangle, then add the target area as a second rectangle to the same GeometryGroup with FillRule.EvenOdd — the target area remains transparent.
Target coordinates are not directly dependent on TranslatePoint. Instead, we get screen coordinates first, then convert them back to the client coordinates of the corresponding TopLevel:
var targetTopLeft = target.PointToScreen(new Point(0, 0));
var origin = relativeTopLevel.PointToClient(targetTopLeft);
var rect = new Rect(origin, target.Bounds.Size);
var result = rect.Inflate(new Thickness(gapX, gapY));
This approach ensures compatibility with targets inside menus, popups, Flyouts, or other popup hosts.
Dynamic Menu Guidance
Menu item guidance is the most important enhancement in this release. The following GIF shows only the menu steps: File menu, Open Folder, Export submenu, Paragraph menu, Format menu, View menu, and Theme submenu.

The issue with menu items is that child MenuItem controls only appear in the visual tree after the parent menu is opened. The solution is to open the parent menu before entering the step, then let the Guide resolve the target with a delay.
Simplified demo code:
<Menu>
<MenuItem x:Name="GuideThemeMenu" Header="Theme Color">
<MenuItem x:Name="GuideThemeBlueItem" Header="Blue" />
<MenuItem x:Name="GuideThemeGreenItem" Header="Green" />
<MenuItem x:Name="GuideThemePurpleItem" Header="Purple" />
</MenuItem>
</Menu>
<codewf:Guide
x:Name="DynamicGuide"
TargetResolveDelay="00:00:00.220"
StepOpening="DynamicGuide_OnStepOpening">
<codewf:GuideStep
Target="{Binding ElementName=GuideThemeMenu}"
Title="Theme Color Menu" />
<codewf:GuideStep
Target="{Binding ElementName=GuideThemeBlueItem}"
Placement="RightBottom"
Title="Blue Theme" />
</codewf:Guide>
Open the parent menu when entering a menu item step:
private void DynamicGuide_OnStepOpening(object? sender, GuideStepEventArgs e)
{
GuideThemeMenu.IsSubMenuOpen = e.Index is >= 1 and <= 3;
Dispatcher.UIThread.Post(
() => GuideThemeMenu.IsSubMenuOpen = true,
DispatcherPriority.Background);
}
The key is TargetResolveDelay. Menu popup creation and layout are not fully synchronous; delaying target resolution slightly improves positioning stability.
Implementation in Vex
Vex's title bar menus are in ShellTitleMenuView.axaml; key menu items are named and exposed to the main window via code-behind:
public MenuItem FileMenuTarget => FileMenuItem;
public MenuItem OpenFolderMenuTarget => OpenFolderMenuItem;
public MenuItem ExportMenuTarget => ExportMenuItem;
public MenuItem TableMenuTarget => TableMenuItem;
public MenuItem LinkMenuTarget => LinkMenuItem;
public MenuItem SourceModeMenuTarget => SourceModeMenuItem;
public MenuItem OutlineMenuTarget => OutlineMenuItem;
public MenuItem ThemeDarkMenuTarget => ThemeDarkMenuItem;
public MenuItem BeginGuideMenuTarget => BeginGuideMenuItem;
Before starting the guide, the main window assigns these controls to the corresponding steps:
private void ConfigureOnboardingGuideTargets()
{
GuideFileMenuStep.Target = TitleMenuView.FileMenuTarget;
GuideFileOpenStep.Target = TitleMenuView.OpenFolderMenuTarget;
GuideFileExportStep.Target = TitleMenuView.ExportMenuTarget;
GuideParagraphMenuStep.Target = TitleMenuView.TableMenuTarget;
GuideFormatMenuStep.Target = TitleMenuView.LinkMenuTarget;
GuideViewMenuStep.Target = TitleMenuView.SourceModeMenuTarget;
GuideViewOutlineMenuStep.Target = TitleMenuView.OutlineMenuTarget;
GuideThemeMenuStep.Target = TitleMenuView.ThemeDarkMenuTarget;
GuideHelpMenuStep.Target = TitleMenuView.BeginGuideMenuTarget;
}
On each step transition, all menus are closed first, then the required menus for the current step are opened. For submenus like theme colors, both parent and child are opened consecutively:
case ThemeColorGuideMenu:
ThemeMenuItem.IsSubMenuOpen = true;
ThemeColorMenuItem.IsSubMenuOpen = true;
break;
The entire pipeline can be summarized in five steps:
GuideStep.Targetpoints to a specificMenuItem.StepOpeningopens the parent menu.TargetResolveDelaywaits for the popup layout.Guideresolves the target, draws the mask, and displays the card.- On step end or guide close, menus are collapsed.
TabItem Switching
Vex's left sidebar uses a TabControl. The guide needs to explain "Files" and "Outline" separately. Both steps reuse the same sidebar target, but the tab is switched before entering the step.

Both steps target the same SidebarGuideTarget:
<codewf:GuideStep
x:Name="GuideSidebarFilesStep"
Target="{Binding ElementName=SidebarGuideTarget}"
Title="{i18n:I18n {x:Static l:VexL.GuideSidebarFilesTitle}}" />
<codewf:GuideStep
x:Name="GuideSidebarOutlineStep"
Target="{Binding ElementName=SidebarGuideTarget}"
Title="{i18n:I18n {x:Static l:VexL.GuideSidebarOutlineTitle}}" />
When entering a step, switch the business state and post a refresh to the UI background queue:
private void PrepareOnboardingGuideStep(IGuideStepOption step)
{
if (DataContext is not MainWindowViewModel viewModel)
{
return;
}
if (ReferenceEquals(step, GuideSidebarFilesStep))
{
viewModel.Layout.ShowFiles();
QueueOnboardingGuideRefresh();
return;
}
if (ReferenceEquals(step, GuideSidebarOutlineStep))
{
viewModel.Layout.ShowOutline();
QueueOnboardingGuideRefresh();
}
}
private void QueueOnboardingGuideRefresh()
{
Dispatcher.UIThread.Post(OnboardingGuide.Refresh, DispatcherPriority.Background);
}
This step is crucial. The tab content needs to be laid out before correct dimensions are available; otherwise, the highlight area may stay at the old position.
First Launch
Vex does not show the guide on every startup. There is a configuration flag:
<add key="HasSeenOnboardingGuide" value="false" />
After the window opens, if the user has not seen the guide, mark it as seen and dispatch the guide once:
private void QueueFirstRunOnboardingGuide()
{
if (_settingsStore is null || _settingsStore.Current.HasSeenOnboardingGuide == true)
{
return;
}
_settingsStore.Update(settings => settings with { HasSeenOnboardingGuide = true });
Dispatcher.UIThread.Post(BeginOnboardingGuide, DispatcherPriority.Background);
}
Afterwards, the user can reopen the guide from the Help menu, which returns to the first step:
private void BeginOnboardingGuide()
{
ConfigureOnboardingGuideTargets();
TitleMenuView.CloseGuideMenus();
OnboardingGuide.GoTo(0);
OnboardingGuide.Show();
}
Known Limitations
Dynamic menu guidance has one boundary case: if the application loses focus during the guide, Avalonia's light-dismiss behavior may close the menu popup first, and subsequent guide steps may also disappear.
The current implementation prioritizes PointerPressed navigation for the previous, next, and finish buttons to avoid being preempted by menu closure when clicking guide buttons. However, when the window genuinely loses focus, the menu popup may still close according to platform rules.
Two potential future directions:
- Strengthen popup target persistence and focus restoration inside
Guide. - After the menu is fully expanded, capture a static snapshot and paste it over the mask layer, then guide based on snapshot positions. This approach is more stable but cannot respond to real menu item interactions.
Summary
Guide now covers common onboarding scenarios in desktop applications: basic multi-step, center-aligned explanations, cover content, custom buttons, non-modal hints, menu item guidance, submenus, TabItem switching, delayed target appearance, and first-run-only display.
After implementing this in Vex, the clearest takeaway is: desktop application onboarding cannot rely solely on static button highlighting. Real entry points are often hidden behind menus, popups, and tabs—the guide control must evolve alongside business state.
Related links:
- CodeWF.AvaloniaControls: https://github.com/dotnet9/CodeWF.AvaloniaControls
- Vex: https://github.com/dotnet9/Vex
- AtomUI: https://github.com/AtomUI/AtomUI
- Issues: https://github.com/dotnet9/CodeWF.AvaloniaControls/issues