Skip to content
Document DB v7.2: Temporal Support, Telemetry Collection, All Calculations, String Based APIs, & Orleans Storage Providers! Feed The Machine Here

TreeView | Blazor Usage

The Blazor <TreeView TItem> component mirrors the MAUI control. It’s strongly typed in the item type, icons are RenderFragment slots, and standard arrow-key navigation is built in.

Install the NuGet package:

Terminal window
dotnet add package Shiny.Blazor.Controls

Add the @using directive — typically in _Imports.razor:

@using Shiny.Blazor.Controls
<TreeView TItem="FileNode"
ItemsSource="rootItems"
ChildrenSelector="@(n => n.Children)"
HasChildrenSelector="@(n => n.IsFolder)"
CanSelectSelector="@(n => !n.IsLocked)"
SelectedItem="selected"
SelectedItemChanged="v => selected = v"
ItemExpanded="OnExpanded">
<ItemTemplate Context="node">
<span>@node.Icon</span>
<span>@node.Name</span>
</ItemTemplate>
</TreeView>
@code {
FileNode? selected;
List<FileNode> rootItems = new() { /* ... */ };
void OnExpanded(TreeItemEventArgs<FileNode> e) => Console.WriteLine($"Expanded {e.Item.Name}");
public class FileNode
{
public string Name { get; set; } = "";
public string Icon { get; set; } = "";
public bool IsFolder { get; set; }
public bool IsLocked { get; set; }
public List<FileNode>? Children { get; set; }
}
}
<TreeView TItem="FileNode"
@ref="tree"
ItemsSource="rootItems"
ChildrenLoader="LoadChildren"
HasChildrenSelector="@(n => n.IsFolder)"
LoadFailed="e => status = $\"Failed: {e.Exception.Message}\"">
<ItemTemplate Context="node">@node.Name</ItemTemplate>
</TreeView>
@code {
TreeView<FileNode>? tree;
string status = "";
async Task<IEnumerable<FileNode>> LoadChildren(FileNode parent)
{
await Task.Delay(500);
return await myService.GetChildrenAsync(parent.Id);
}
// Programmatic API:
async Task RefreshNode(FileNode n) => await tree!.RefreshAsync(n);
async Task ExpandAll() => await tree!.ExpandAllAsync();
}

You can mix sync and lazy branches in the same tree — ChildrenSelector is consulted first, then ChildrenLoader is the fallback when the selector returns null.

<TreeView TItem="FileNode" ItemsSource="rootItems" ChildrenSelector="@(n => n.Children)">
<ExpandedIcon>
<i class="fa-solid fa-chevron-down" style="color:#7C3AED"></i>
</ExpandedIcon>
<CollapsedIcon>
<i class="fa-solid fa-chevron-right" style="color:#7C3AED"></i>
</CollapsedIcon>
<RetryIcon>
<i class="fa-solid fa-rotate-right" style="color:#DC2626"></i>
</RetryIcon>
<LoadingTemplate>
<span class="spinner-border spinner-border-sm" />
</LoadingTemplate>
<ItemTemplate Context="node">
@node.Name
</ItemTemplate>
</TreeView>

If no slot is supplied, the control falls back to the built-in glyphs (, , ) coloured via the ChevronColor parameter.

<TreeView TItem="FileNode"
SelectionMode="BlazorTreeSelectionMode.Single"
SelectedItem="selected"
SelectedItemChanged="v => selected = v"
ItemsSource="rootItems" />

For multi-select:

<TreeView TItem="FileNode"
SelectionMode="BlazorTreeSelectionMode.Multiple"
SelectedItems="selected"
SelectedItemsChanged="v => selected = v"
ItemsSource="rootItems" />
@code {
IList<FileNode> selected = new List<FileNode>();
}

Drag/drop runs on native HTML5 drag events registered by a small JS module that loads automatically when EnableDragDrop="true" — not Blazor’s synthetic events, which can’t call dataTransfer.setData() and therefore break Safari and Firefox.

ItemDropped receives TreeItemDroppedEventArgs<TItem> with SourceItem, TargetItem, and Position (BlazorTreeDropPosition.Above / Below to reorder among siblings, Into to move into a folder). Rows show drop indicators while dragging: a horizontal line for above/below, a highlight for into.

<TreeView TItem="FileNode"
@ref="tree"
EnableDragDrop="true"
ItemDropped="OnDropped"
ItemsSource="rootItems"
ChildrenSelector="@(n => n.Children)" />
@code {
TreeView<FileNode>? tree;
async Task OnDropped(TreeItemDroppedEventArgs<FileNode> e)
{
var srcList = FindParentList(e.SourceItem);
if (e.Position == BlazorTreeDropPosition.Into)
{
srcList.Remove(e.SourceItem);
e.TargetItem.Children ??= new();
e.TargetItem.Children.Add(e.SourceItem);
}
else
{
var tgtList = FindParentList(e.TargetItem);
srcList.Remove(e.SourceItem);
var idx = tgtList.IndexOf(e.TargetItem);
tgtList.Insert(e.Position == BlazorTreeDropPosition.Above ? idx : idx + 1, e.SourceItem);
}
await tree!.ReloadAsync(); // rebuilds while preserving expansion/selection
if (e.Position == BlazorTreeDropPosition.Into)
await tree!.ExpandAsync(e.TargetItem);
}
}

Drops onto descendants are rejected automatically.

The Blazor TreeView is focusable (tabindex="0") and supports:

KeyAction
/ Move focus to previous/next visible row
Expand collapsed node, or move into first child if already expanded
Collapse expanded node, or move focus to parent
Enter / SpaceSelect the focused row
Home / EndJump to first / last visible row
ParameterTypeDefaultDescription
ItemsSourceIEnumerable<TItem>Source for root items
RootLoaderFunc<Task<IEnumerable<TItem>>>Async loader for roots (overrides ItemsSource)
ChildrenSelectorFunc<TItem, IEnumerable<TItem>?>Sync children getter
ChildrenLoaderFunc<TItem, Task<IEnumerable<TItem>>>Async children loader (fallback when selector returns null)
HasChildrenSelectorFunc<TItem, bool>Whether the row should render a chevron
CanExpandSelectorFunc<TItem, bool>Gate expansion gesture
CanSelectSelectorFunc<TItem, bool>Gate selection gesture
SelectionModeBlazorTreeSelectionModeSingleNone / Single / Multiple
IndentSizedouble20Pixels of indent per depth level
ChevronSizedouble14Pixels
ChevronColorstring"#666"CSS color used for glyph fallbacks
ShowGuideLinesboolfalseVertical connector lines
EnableDragDropboolfalseEnables native HTML5 drag/drop (via JS interop) with the ItemDropped event
ItemDroppedEventCallback<TreeItemDroppedEventArgs<TItem>>Fires after a drop with SourceItem / TargetItem / Position
CssClassstring?Extra class added to the root <div>
await tree.ExpandAsync(item);
await tree.CollapseAsync(item);
await tree.ExpandAllAsync();
tree.CollapseAll();
await tree.RefreshAsync(item); // drops cache for this node
await tree.ReloadAsync(); // rebuilds from data, preserving expansion/selection for items that still exist
var node = tree.FindNode(item); // locate the wrapper node for any source item