Triggers
Trigger is a [Flags] enum, so an icon can loop and respond to a tap.
[Flags]public enum MotionTrigger{ Manual = 0, Loop = 1, Hover = 2, Press = 4, Appear = 8}The default is Hover | Press — hover for desktop, press for touch, so an icon does something sensible everywhere without being told.
Runs continuously. Interval inserts a resting gap between cycles, which is almost always what you want: a bell that rings without pause reads as broken.
<shiny:MotionIconView Icon="bell" Trigger="Loop" Interval="0:0:1.5" />The gap is folded into the animation itself rather than scheduled by a timer. That is what keeps the two hosts honest — a CSS animation has no way to pause between iterations, so a spec that expressed the gap externally would need a JavaScript timer on the web and a dispatcher timer on MAUI, and the two would drift apart. Squeezing the keys into the front of a longer cycle and holding the resting pose through the remainder produces the same result with nothing but animation-iteration-count: infinite.
Runs while the pointer is over the icon. When the pointer leaves, the icon finishes the cycle it is in and then stops, so a half-swung bell settles upright instead of snapping.
<MotionIcon Icon="settings" Trigger="MotionTrigger.Hover" />Hover needs a pointer, so pair it with Press for touch — which the default already does.
One play per tap or click.
<shiny:MotionIconView Icon="heart" Trigger="Press" Command="{Binding LikeCommand}" />On MAUI a Command is invoked whether or not Press is set, so the icon can act as a button while some other trigger drives its motion. On Blazor the equivalent is OnClick.
Press only hears presses that land on the icon itself. An icon sitting inside a larger tap target — a button, a list row — should be Manual, with the host playing it, or a click on the label around it will animate nothing. That is exactly what ShinyButton does with its slot icons on both hosts.
RepeatCount controls how many cycles a triggered play runs for — set it to 2 for a double-take, or 0 to keep going until something stops it.
Appear
Section titled “Appear”Plays once when the icon first becomes visible. On the web this is an IntersectionObserver; on MAUI it fires when the view loads.
<MotionIcon Icon="check" Trigger="MotionTrigger.Appear" Title="Saved" />It is deliberately one-shot. An icon that replays every time it scrolls past is a distraction; if you want that, drive IsPlaying yourself.
Manual, and busy states
Section titled “Manual, and busy states”Manual is the absence of any automatic trigger. Drive it with the methods, or — far more often — bind IsPlaying:
<shiny:MotionIconView Icon="loader" Trigger="Manual" IsPlaying="{Binding IsBusy}" /><MotionIcon Icon="loader" Trigger="MotionTrigger.Manual" IsPlaying="@busy" />While the flag is set the icon loops; when it clears, the icon returns to its resting pose. For the spinner that means settling back into a full three-quarter ring rather than snapping wider the instant it stops.
| Method | Does |
|---|---|
Play() |
Plays for RepeatCount cycles. |
Loop() |
Blazor only — plays continuously. |
PlayAsync() |
MAUI only — completes when the animation finishes. |
Stop() |
Stops now and returns to rest. |
StopAtCycleEnd() |
Lets the current cycle finish, then stops. |
Reset() |
MAUI only — back to rest without changing playback. |
StopAtCycleEnd() is what the hover trigger uses internally, and it is usually the better choice when a user-visible state ends: it costs at most one more cycle and always finishes on the resting pose.
Scrubbing (MAUI)
Section titled “Scrubbing (MAUI)”Because evaluating the animation is a pure function of progress — nothing reads the previous frame — an icon can be driven directly from a slider or a gesture:
<shiny:MotionIconView Icon="menu" Trigger="Manual" Progress="{Binding Source={x:Reference Scrubber}, Path=Value}" />
<Slider x:Name="Scrubber" Minimum="0" Maximum="1" />Dragging the slider morphs the hamburger into a cross and back. The same property reports position while the animation plays.


