SkeletonView
Skeleton placeholders mimic the shape of content that is still loading, improving perceived performance compared to a spinner. Uno Toolkit generates the placeholders for you from the real layout of your content: you never declare skeleton shapes.
Three building blocks cover the different ways content can be loading:
| Type | Use it when | How it works |
|---|---|---|
SkeletonView |
The real content is already in the visual tree, bound to data that is still loading (or being refreshed). | Wraps the content, hides it while IsLoading is true, and overlays one placeholder per visual element at its actual position. Empty lists can get placeholder rows from their ItemTemplate. |
SkeletonPresenter |
The real content is not in the tree while loading, e.g. inside a loading slot or a ProgressTemplate. |
Instantiates a private, data-less copy of a DataTemplate and generates placeholders from it, stamping placeholder rows into empty lists. |
Skeleton attached properties |
You need to tune generation. | Ignore, Shape, PlaceholderCount. |
For MVUX's FeedView, see FeedView.
xmlns:utu="using:Uno.Toolkit.UI"
How placeholders are generated
Understanding the generation rules helps you get the skeleton you want with little or no tuning.
- The visual tree is walked from the content root. Collapsed elements and elements marked with
utu:Skeleton.Ignore="True"are skipped along with their subtree. - Leaves get exactly one placeholder. A leaf is a
TextBlock, anImage, anyShape(Ellipse,Rectangle,Path, ...), a button (ButtonBase), or an input control whose value is its own chrome:TextBox,PasswordBox,RichEditBox,AutoSuggestBox,ComboBox,NumberBox,Sliderand the otherRangeBasecontrols,ProgressRing,ToggleSwitch,DatePicker,TimePicker,CalendarDatePicker,CalendarView,RatingControl,ColorPicker. Leaves are not descended into, so aButtonproduces a single button-sized placeholder regardless of its content, and aTextBoxone placeholder on every platform. - Containers contribute their children, not themselves. Panels and borders produce no placeholder of their own. For content controls (e.g.
ListViewItem,ContentControl, cards), only the content is walked; the control's template chrome (backgrounds, selection and focus visuals) is ignored. Content hidden by that chrome (e.g. a collapsedExpander) is not mirrored. - Each placeholder matches the leaf's position and size within the control, clipped to what can be seen of it: a row scrolled half out of a list produces half a placeholder, a row realized ahead of the viewport produces none, and a circle that gets clipped becomes a rectangle. Scrolling viewports and explicit
Clips are honored; theSkeletonView's own bounds always are. - Leaves without a value yet fall back to their layout slot. An empty data-bound
TextBlockor a source-lessImagemeasures as (almost) nothing, so the space the layout granted it is used instead:- width: the slot's width (a stretched empty
TextBlockproduces a full-width line); when the slot gives no width either, aTextBlockuses a default line width of 100; - height: for a
TextBlock, one text line derived from itsFontSize; otherwise the slot's height. Give such elements an explicit size, alignment orMinWidthwhen you want a more realistic placeholder.
- width: the slot's width (a stretched empty
- Shape: an
Ellipseproduces a circle; every other leaf produces a rounded rectangle usingPlaceholderCornerRadius. Force either withutu:Skeleton.Shape. A circle is sized to the smaller dimension and centered. - Empty lists with
utu:Skeleton.PlaceholderCountget placeholder rows generated from theirItemTemplate(see Empty lists). Other empty lists have nothing to mirror and produce no placeholders.
Placeholders are regenerated on every layout pass while loading (the content can change without resizing anything: rows arriving in a list, containers being recycled), and when an appearance property changes. A pass where nothing moved costs a walk of the content and nothing else.
While loading, the content stays in the tree, so layout and bindings keep working, but it is not hit-testable. Only the elements covered by a placeholder are hidden: everything else stays visible, including ignored elements and the backgrounds and borders of containers (for example a card keeps its frame, with placeholders inside). Covered elements are hidden by setting their Opacity to 0; their original Opacity (local value or binding) is restored once loaded, and whenever the SkeletonView leaves the visual tree.
SkeletonView
Wraps live content and replaces it visually with generated placeholders while loading.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
IsLoading |
bool |
true |
Whether the placeholders are shown instead of the content. Note the default: a SkeletonView starts in its loading state. Bind it to your busy flag, or set Source. |
Source |
ILoadable |
null |
An ILoadable (such as an async command) whose IsExecuting state drives IsLoading. When set, IsLoading follows IsExecuting. IsExecutingChanged must be raised on the UI thread: wrap a source that completes elsewhere in a LoadableSource. See LoadingView for ILoadable implementations. |
EnableShimmer |
bool |
true |
Whether the shimmer animation plays while loading. Set to false for static placeholders (e.g. to reduce motion). |
ShimmerDuration |
Duration |
0:0:1.5 |
Duration of one shimmer sweep across the control. Larger values produce a slower sweep. |
SkeletonBackground |
Brush |
SkeletonViewBackground resource |
Fill brush of the placeholders. |
ShimmerBrush |
Brush |
SkeletonViewShimmerBrush resource |
Brush of the highlight band swept across the placeholders. Typically a horizontal LinearGradientBrush fading in and out of transparency. Set to null to remove the band entirely. |
PlaceholderCornerRadius |
CornerRadius |
SkeletonViewCornerRadius resource (4) |
Corner radius of rectangular placeholders. Circles are unaffected. |
As a ContentControl, SkeletonView also accepts Content, ContentTemplate, HorizontalContentAlignment/VerticalContentAlignment (both Stretch by default), Background, Padding, etc.
Basic usage
Write the content once, as regular XAML bound to your data, and wrap it:
<utu:SkeletonView IsLoading="{Binding IsBusy}">
<StackPanel Spacing="12">
<StackPanel Orientation="Horizontal" Spacing="12">
<Ellipse Width="48" Height="48" Fill="{ThemeResource SystemControlHighlightAccentBrush}" />
<StackPanel VerticalAlignment="Center" Spacing="4">
<TextBlock Text="{Binding Title}" Style="{StaticResource SubtitleTextBlockStyle}" />
<TextBlock Text="{Binding Subtitle}" Style="{StaticResource CaptionTextBlockStyle}" />
</StackPanel>
</StackPanel>
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
<Button Content="Some action" />
</StackPanel>
</utu:SkeletonView>
While IsBusy is true, this shows a circle, two text lines, a full-width text line for the (still empty) description, and a button-shaped placeholder.
Driven by an ILoadable
Instead of a flag, let any ILoadable (e.g. an async command) drive the loading state:
<Button Content="Reload" Command="{Binding LoadDataCommand}" />
<utu:SkeletonView Source="{Binding LoadDataCommand}">
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
</utu:SkeletonView>
Refreshing loaded content
Because placeholders are generated from the live layout, toggling IsLoading back on over content that is already loaded produces a skeleton that matches it exactly: the same number of list items, and text lines matching the length of the current text. This makes SkeletonView well suited to refresh scenarios.
Empty lists
On a first load, a list has no items yet, so there are no rows to mirror. Set utu:Skeleton.PlaceholderCount on the list (or on any ancestor) to get placeholder rows anyway. The list can be nested anywhere in the content:
<utu:SkeletonView Source="{Binding LoadPeopleCommand}">
<Grid RowSpacing="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Team" utu:Skeleton.Ignore="True" />
<ListView Grid.Row="1"
utu:Skeleton.PlaceholderCount="3"
ItemsSource="{Binding People}"
ItemTemplate="{StaticResource PersonRowTemplate}" />
<Button Grid.Row="2" Content="Invite someone" />
</Grid>
</utu:SkeletonView>
While loading, every ItemsControl, ListView, GridView and ItemsRepeater in the content that has an ItemTemplate, no items, and a PlaceholderCount (set on itself or an ancestor, locally or through a Style):
- gets
PlaceholderCountrows built from a private copy of the list (sameItemTemplate, plusItemContainerStyle,ItemsPanel,StyleandPaddingfor anItemsControl, orLayoutfor anItemsRepeater) filled withnullitems, so bound values are empty and follow the empty-value rules; - reserves the rows' space when it is sized by its content (e.g. in an
Autorow or a verticalStackPanel): itsMinHeightis raised temporarily, up to itsMaxHeight, so the content below it moves down as if the rows existed. A list that already has more space than it needs (e.g. stretched in a*row) keeps its size, and rows that don't fit are clipped. A list with an explicitHeightis never resized; - goes back to normal once loading ends or its items arrive: the rows are dropped, its own items are mirrored instead, and its
MinHeight(local value or binding) is restored.
The list's ItemsSource, selection and position in the tree are never changed.
Notes:
- The list needs a width: a list that is 0 wide (e.g. unconstrained in a horizontal
StackPanel) gets no rows. - A list's
HeaderandFooterare not mirrored; placeholder rows start at the top of the list. ItemTemplateSelectorandItemContainerStyleSelectorare not used for placeholder rows (their items arenull): setItemTemplate, andItemContainerStyleif needed. The template must tolerate anulldata context.- Other
ItemsControl-derived controls (ListBox,FlipView, custom subclasses ofItemsControl) don't get rows; aDebuglog entry says so. Subclasses ofListViewandGridVieware treated asListViewandGridView: aStyletargeting the subclass is not applied to the placeholder copy.
Appearance
Every appearance property can be set per instance:
<utu:SkeletonView IsLoading="{Binding IsBusy}"
SkeletonBackground="#D7E3F4"
PlaceholderCornerRadius="12"
ShimmerDuration="0:0:3">
<utu:SkeletonView.ShimmerBrush>
<LinearGradientBrush StartPoint="0,0.5" EndPoint="1,0.5">
<GradientStop Offset="0" Color="#00FFFFFF" />
<GradientStop Offset="0.5" Color="#AAFFFFFF" />
<GradientStop Offset="1" Color="#00FFFFFF" />
</LinearGradientBrush>
</utu:SkeletonView.ShimmerBrush>
<!-- content -->
</utu:SkeletonView>
<!-- Static placeholders, no animation -->
<utu:SkeletonView IsLoading="{Binding IsBusy}" EnableShimmer="False">
<!-- content -->
</utu:SkeletonView>
To change the look app-wide, override the lightweight styling resources instead.
SkeletonPresenter
A SkeletonView that generates its skeleton from a DataTemplate instead of live content. Use it wherever the real content does not exist yet while loading: a LoadingView's LoadingContent, a hand-written ProgressTemplate, or a control template.
If the real content is in the tree while loading, and only its list is empty, you don't need a SkeletonPresenter: wrap the content in a SkeletonView and set utu:Skeleton.PlaceholderCount on the list (see Empty lists).
SkeletonPresenter derives from SkeletonView, so it inherits all of its properties and lightweight styling. IsLoading defaults to true, which is what you want for a loading slot. When the slot is hidden rather than removed (a LoadingView keeps its LoadingContent in the tree once loaded), the shimmer follows ProgressExtensions.IsActive like a ProgressRing would: LoadingView sets it on its loading slot, so nothing animates out of sight. Elsewhere, set IsLoading to false to stop it.
Template
The skeleton is derived from the presenter's ContentTemplate. In a control template, template-bind it to the control's value template (e.g. ContentTemplate="{TemplateBinding ValueTemplate}"). A presenter without a ContentTemplate (and without element Content) logs a warning when it loads.
The presenter's Content (if any) becomes the template's data context. Leave it empty, or pass an object whose data is not loaded yet, so that bound values are empty and placeholders follow the empty-value rules.
Placeholder rows for lists
A template typically contains a list whose items are not loaded yet. Every ItemsControl (including ListView/GridView) and ItemsRepeater in the instantiated template that has an ItemTemplate but no items and no ItemsSource receives utu:Skeleton.PlaceholderCount dummy items, so the list realizes placeholder rows from its own ItemTemplate. This only ever affects the presenter's private instantiation of its ContentTemplate, never your live content: elements set directly as the presenter's Content are treated like the content of any SkeletonView (an empty list in them gets rows through PlaceholderCount instead).
ItemTemplateSelector is not used for placeholder rows: set ItemTemplate.
Usage
<Page.Resources>
<DataTemplate x:Key="PersonRowTemplate">
<StackPanel Orientation="Horizontal" Spacing="12">
<Ellipse Width="40" Height="40" Fill="{ThemeResource SystemControlHighlightAccentBrush}" />
<StackPanel VerticalAlignment="Center" Spacing="4">
<TextBlock Text="{Binding Name}" />
<TextBlock Text="{Binding Title}" FontSize="12" />
</StackPanel>
</StackPanel>
</DataTemplate>
</Page.Resources>
<utu:SkeletonPresenter utu:Skeleton.PlaceholderCount="3">
<utu:SkeletonPresenter.ContentTemplate>
<DataTemplate>
<ListView ItemTemplate="{StaticResource PersonRowTemplate}" />
</DataTemplate>
</utu:SkeletonPresenter.ContentTemplate>
</utu:SkeletonPresenter>
Inside a LoadingView
Reuse the same item template for both the loaded list and its loading skeleton:
<utu:LoadingView Source="{Binding LoadPeopleCommand}">
<ListView ItemsSource="{Binding People}" ItemTemplate="{StaticResource PersonRowTemplate}" />
<utu:LoadingView.LoadingContent>
<utu:SkeletonPresenter utu:Skeleton.PlaceholderCount="3">
<utu:SkeletonPresenter.ContentTemplate>
<DataTemplate>
<ListView ItemTemplate="{StaticResource PersonRowTemplate}" />
</DataTemplate>
</utu:SkeletonPresenter.ContentTemplate>
</utu:SkeletonPresenter>
</utu:LoadingView.LoadingContent>
</utu:LoadingView>
Skeleton attached properties
The static Skeleton class provides attached properties that tune generation.
| Property | Type | Default | Set on | Description |
|---|---|---|---|---|
Skeleton.Ignore |
bool |
false |
Any element inside the content | Excludes the element and its subtree from generation: no placeholder is produced for it, and it stays visible while loading. Use it for static parts that do not depend on the data being loaded, such as headers or labels. |
Skeleton.Shape |
SkeletonShape |
Auto |
Any element inside the content | Forces the placeholder shape. Any value other than Auto also makes the element a leaf: a single placeholder covers it and its subtree is not visited. Use it to collapse a complex element (e.g. an avatar made of an Image inside a Border) into one shape. |
Skeleton.PlaceholderCount |
int |
0 |
In a SkeletonView: an empty list, or any of its ancestors. For a SkeletonPresenter: the presenter or any of its ancestors (typically the control whose template hosts the presenter) |
Number of placeholder rows generated for empty lists while loading. The nearest element with a value applies, whether set locally or through a Style; a 0 set locally disables rows, and vetoes an ancestor's value (a 0 from a Style is the same as no value). In a SkeletonView, rows are opt-in: a list without a value gets none. A SkeletonPresenter stamps its template's lists with the nearest value (the presenter itself first, then its ancestors), else SkeletonPresenter.DefaultPlaceholderCount (4). |
SkeletonShape
| Value | Placeholder |
|---|---|
Auto |
Inferred: an Ellipse produces a circle, every other leaf a rounded rectangle. |
Rectangle |
A rectangle with PlaceholderCornerRadius, covering the element's bounds. |
Circle |
A circle whose diameter is the smaller of the element's width and height, centered in its bounds. |
Overrides
<utu:SkeletonView IsLoading="{Binding IsBusy}">
<StackPanel Spacing="12">
<!-- static header: stays visible while loading -->
<TextBlock utu:Skeleton.Ignore="True" Text="Profile" />
<!-- one circle instead of the image inside -->
<Border utu:Skeleton.Shape="Circle" Width="64" Height="64" CornerRadius="32">
<Image Source="{Binding AvatarUrl}" Stretch="UniformToFill" />
</Border>
<!-- one block instead of every chip inside -->
<ItemsControl utu:Skeleton.Shape="Rectangle" Height="32" ItemsSource="{Binding Tags}" />
</StackPanel>
</utu:SkeletonView>
FeedView (Uno Extensions)
MVUX's FeedView is supported through the SkeletonFeedViewStyle style provided by Uno.Extensions, which uses SkeletonPresenter for the initial load and SkeletonView for refreshes. See the FeedView documentation for how to apply it.
Lightweight Styling
| Key | Type | Value |
|---|---|---|
SkeletonViewBackground |
SolidColorBrush |
#14000000, black at 8% (Dark: #1AFFFFFF, white at 10%) |
SkeletonViewShimmerBrush |
LinearGradientBrush |
Horizontal transparent → #A6FFFFFF → transparent (Dark: #1AFFFFFF) |
SkeletonViewCornerRadius |
CornerRadius |
4 |
The default brushes are translucent, so placeholders take on the surface they sit on (a card, a tinted page, or the Material and Cupertino backgrounds) without per-theme overrides.
These resources apply to both SkeletonView and SkeletonPresenter. Override them in your application resources to restyle every skeleton:
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="SkeletonViewBackground" Color="#E8EEF6" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="SkeletonViewBackground" Color="#2B3442" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
Limitations and troubleshooting
- Nothing is shown while loading. The content has no leaves yet, or they are collapsed. For an empty list, set
utu:Skeleton.PlaceholderCounton it (see Empty lists). For content that isn't in the tree at all while loading, use aSkeletonPresenter. - An empty list gets no rows. Check that it has an
ItemTemplateand a width, and thatPlaceholderCountis set on it or an ancestor and is greater than 0. - A placeholder covers a whole area. An element in the content is a
Shapeor button spanning that area (e.g. a decorative backgroundRectangle). Mark it withutu:Skeleton.Ignore="True"to keep it visible as-is instead. - Text placeholders are full width. An empty, horizontally stretched
TextBlockfalls back to its layout slot. SetHorizontalAlignment="Left"with aMinWidth, or aWidth, for a shorter line. - An element's
Opacityis changed while loading. Covered elements are hidden throughOpacity, overriding values the app sets or animates on them during that time; the original value or binding is restored once loaded. Avoid drivingOpacityof covered elements while loading. - The skeleton always shows.
IsLoadingdefaults totrue; bind it or setSource. - Elements outside the control are not covered. Placeholders are confined to the
SkeletonView; wrap each area that loads. - Nested skeletons own their content. A
SkeletonView(orSkeletonPresenter) inside the content is left alone: the outer view produces no placeholders for it, and each view hides and restores its own elements. - Keyboard focus can still reach covered content. Hidden elements are not hit-testable, but a
Tabcan still focus a covered control, as withLoadingView. BindIsEnabledof such controls to your loading flag if that matters. The placeholders themselves are not exposed to assistive technologies. - Warnings in the log.
Uno.Toolkit.UI.SkeletonPresenterwarns when it has noContentTemplateto derive the skeleton from. Lists skipped for an unsupported kind are logged atDebuglevel. - Virtualized lists only produce placeholders for realized items, which is usually what fills the viewport.
Samples
See the SkeletonView page of the Uno Toolkit sample app for a live demo of SkeletonView and SkeletonPresenter, including generation overrides, appearance, empty lists and LoadingView integration.