horussoftwareuy/xamarin.forms.skeleton/skeleton
Claude Code and agent skill by horussoftwareuy/xamarin.forms.skeleton
- community
Checking that the SKILL.md file is still in the repository.
Add skeleton loading placeholders to .NET MAUI views using the HorusStudio.Maui.Skeleton package. Use this whenever someone wants a loading state, skeleton screen, shimmer effect, content placeholder, or "grey boxes while data loads" in a MAUI app, and whenever they mention Skeleton.IsBusy, Skeleton.IsParent, or the HorusStudio.Maui.Skeleton package by name. Also use it when someone asks to improve perceived loading time on a XAML page, or is replacing an ActivityIndicator or spinner with something better — the API has several traps that produce placeholders which look correct but leak the real content through, and this skill explains how to avoid them.
Install
npx vskill@latest install horussoftwareuy/xamarin.forms.skeleton/skeletonSKILL.md
Skeleton loading for .NET MAUI
HorusStudio.Maui.Skeleton turns controls a developer already has into grey placeholders while data loads, instead of hiding the page behind a spinner. There are no new controls to adopt: you attach properties to the existing XAML.
Work directly on the view the developer names. Do not introduce a base class, a wrapper control, or a new view model — the point of this library is that neither is needed.
Setup
The package is HorusStudio.Maui.Skeleton, version 3.0.0 or later, which requires .NET 8 or newer. Apps on .NET 6 or 7 have to stay on 2.0.0.
Every XAML file that uses it needs this namespace:
The assembly is Maui.Skeleton even though the package is HorusStudio.Maui.Skeleton. Do not write clr-namespace:Xamarin.Forms.Skeleton — that is the legacy Xamarin.Forms package and the type does not exist in the MAUI assembly.
The five properties
| Property | Type | What it does | |
|---|---|---|---|
Skeleton.IsBusy | bool | The switch. Everything else only takes effect while this is true. | |
Skeleton.BackgroundColor | Color | The placeholder colour to paint while busy. | |
Skeleton.Hide | bool | Hide the element entirely while busy, instead of painting it. | |
Skeleton.IsParent | bool | On a container, stop it from fading out its own content. | |
Skeleton.Animation | BaseAnimation | Optional motion while busy. |
`IsBusy` is not inherited. Every element that should react needs its own sk:Skeleton.IsBusy="{Binding ...}". Setting it once on an outer layout does nothing for the elements inside. This is the single most common mistake — a page where only the outer container carries the binding renders no placeholders at all.
Never set it on the page itself. The properties are only valid on View and its subclasses; ContentPage is not one, and the library throws NotSupportedException the moment the binding resolves. Attach to the layouts and controls inside the page, not to the page.
Bind IsBusy to the loading property the view model already exposes (IsLoading, IsBusy, IsRefreshing, whatever it is called). Only add a new property if there genuinely is none, and say so.
Deciding what each element gets
Go element by element through the view. The right treatment depends on what the element is.
A container — Grid, StackLayout, VerticalStackLayout, FlexLayout, Border, Frame, ContentView, ScrollView. One property decides how it behaves, and the question it answers is whether the container is one shape or several shapes:
Without IsParent the container fades its content out, so only its BackgroundColor remains — one solid block. With IsParent="True" the content is left alone and every child has to declare its own treatment. The container still paints its colour and runs its animation either way; IsParent only governs whether it touches what is inside.
A container without IsParent and without BackgroundColor leaves an empty hole: the content fades but nothing is painted in its place. If you leave IsParent off, give it a colour.
Which containers fade their content changed in 3.0.0. Up to 2.0.0 only a Layout did, such as Grid or StackLayout. Now a view holding a single piece of content does too. The test is two conditions: a View implementing IContentView, whose presented content is a View. Border, Frame, ContentView, ScrollView, RefreshView and SwipeView are the ones that meet both. Implementing the interface is not enough on its own: a RadioButton does, but with its default template it presents nothing, so nothing of it fades. Those previously painted the placeholder and left their content visible on top of it, so markup written against 2.0.0 usually worked around it with Skeleton.Hide="True" on every child. From 3.0.0 they behave like the rest, so the workaround is redundant: harmless, but worth removing when you touch the file. If someone reports that content which used to stay visible now disappears, the fix is IsParent="True" on that container, not Hide on the children.
A `Label` or a `Button` paints its own placeholder — the library makes the text transparent, so the background colour becomes the grey bar:
Give it a WidthRequest if the placeholder would otherwise collapse to nothing. A label bound to a string that is empty while loading has no width, so there is nothing to paint.
Anything that should simply disappear rather than turn grey — a chevron, a badge, a decorative icon — takes Hide instead:
Hide sets IsVisible = false, so the element also stops taking up space and the layout closes up around it. Reach for it when the gap is better than a placeholder; inside a container that already fades its content, it is redundant.
Animations
Attach one per element with the markup extension:
Source is the extension's content property, so {sk:DefaultAnimation Fade} is equally valid when you want the defaults.
| Source | What moves | Parameter means | Default | |
|---|---|---|---|---|
Fade | opacity | opacity to fade to | 0.6 | |
Beat | scale | scale to grow to | 1.03 | |
VerticalShake | position | offset in units, up and down | 15 | |
HorizontalShake | position | offset in units, left and right | 10 | |
Shimmer | a band of light across the placeholder | nothing, see below | — | |
Aurora | a wide field of colour drifting back and forth | nothing, see below | — | |
Tint | the whole placeholder washing to a colour and back | nothing, see below | — | |
None | nothing | — | — |
Shimmer, Aurora and Tint
These three work differently from the other four and share three rules.
It has to go on the element that shows the placeholder colour. The band is painted into that element's own background, so it does not carry down to children the way Fade and Beat do. On a transparent container it does nothing at all.
A `Background` you set yourself comes back, but not a binding behind it. These animations paint the element's background, so anything already there is saved and restored when loading finishes. If that background came from a Binding or a DynamicResource, the value returns but the expression does not, and it stops updating. Use sk:Skeleton.BackgroundColor for the placeholder and leave Background alone on elements you animate this way.
It does not work on `Frame`. Frame is deprecated in MAUI and its renderer does not repaint when the background is replaced, so the band never moves and you get a static placeholder with no error. Use Border, or any other non-legacy control.
They ignore Parameter and take settings of their own instead:
| Setting | Values | Applies to | Default | |
|---|---|---|---|---|
Direction | Horizontal, Vertical, Diagonal, DiagonalReverse | Shimmer, Aurora | Horizontal | |
SweepColors | two or three colours, #AARRGGBB, comma separated | all three | follows the placeholder |
Tint has no direction: nothing about it moves for one to apply to. It reads only the middle colour of SweepColors, the one it washes to, so the same palette can be handed to any of the three.
Interval also reads differently here. For Fade or Beat it is half a cycle, because those go out and back. For these three it is the whole movement: one pass for Shimmer, which travels one way, and out and back for Aurora and Tint. 1600 is a good value for any of them.
What separates them. Shimmer sends a band the width of the element across it and off the other side, so there is a moment between passes with nothing on screen; it reads as a sweep going by. Aurora holds a gradient far wider than the element and pans a window over it, so colour is always present and only shifts; it reads as a slow wash. Tint has no gradient and nothing travels: the whole placeholder takes on a colour and lets it go.
Reach for Shimmer on a plain grey placeholder, Aurora when the loading state should carry the product's colours, and Tint when it should stay in the background. Tint uses a single colour, the middle of SweepColors, so the same palette can be handed to any of the three.
Left alone, SweepColors picks a band that contrasts with the placeholder: light over a dark placeholder, dark over a light one. That is usually what you want, and it means a placeholder bound with AppThemeBinding gets a correct sweep in both themes without any extra work:
To choose the colours yourself, write two or three of them. Two are read as edge and peak and mirrored; three are taken as written, which is how the band gets different colours at its two ends. The alpha of each is composited over the placeholder, so they describe light falling on it:
Note the quotes. A markup extension separates its properties with commas, so a value containing commas has to be quoted or the parser reads it as several properties.
SweepColors is a property of the markup extension, not a bindable property, so {AppThemeBinding} inside it is evaluated once and will not follow a theme change. If you need specific colours per theme rather than the automatic contrast, build the animation in code and bind it:
Interval is the duration in milliseconds of each half of a cycle, so a Fade at 600 takes 1200 ms to go down and back. It defaults to 500.
Keep animations subtle. Beat at 1.03 and Fade at 0.6 are the shipped defaults for a reason — a loading placeholder that pulses hard is more distracting than a spinner. Prefer Fade, and reach for the shakes only when the developer asks for something livelier.
{sk:DefaultAnimation None} resolves to no animation at all, which is a reasonable default for large surfaces like a header image where motion is more noticeable than helpful.
Animations are per element, and a group of placeholders animating in unison reads better than each one drifting on its own timing. Use the same Source and Interval across a view unless there is a reason not to.
A complete example
A list row, before:
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/skeleton/SKILL.md
Popularity
- Repository stars
- 469
- Repository forks
- 54
- 7-day trend score
- 31
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/horussoftwareuy/xamarin.forms.skeleton/skeleton)