Animations can make or break a mobile app. Seriously — a well-placed fade or a smooth page transition is often the difference between an app that feels polished and one that feels like a homework project. In .NET MAUI, you've got a surprisingly rich set of animation tools at your disposal, from dead-simple one-liner view extensions all the way to fully custom frame-by-frame animations and Lottie vector files.
This guide walks through every animation approach available in .NET MAUI 10, with code you can actually drop into your projects. Whether you're after a subtle button press effect or a full-blown onboarding sequence, there's something here for you.
Built-In Animations with ViewExtensions
The quickest way to get animations going in .NET MAUI is through the ViewExtensions class. These are extension methods that work on any VisualElement and return Task<bool>, so they're fully awaitable. The return value is false when the animation completes normally and true if it was cancelled.
Honestly, for 80% of use cases, these built-ins are all you need.
FadeTo — Opacity Animations
FadeTo animates the Opacity property from its current value to a target. It's perfect for revealing content on page load or making things disappear gracefully.
// Fade in from invisible
myImage.Opacity = 0;
await myImage.FadeTo(1, 500); // 500ms duration
// Fade out
await myImage.FadeTo(0, 300);
TranslateTo — Movement Animations
TranslateTo changes the TranslationX and TranslationY properties, sliding an element across the screen without touching the layout. Great for slide-in panels, swipe feedback, or parallax effects.
// Slide a card in from the right
card.TranslationX = 300;
await card.TranslateTo(0, 0, 400, Easing.CubicOut);
// Shake effect (horizontal wiggle)
await label.TranslateTo(-10, 0, 50);
await label.TranslateTo(10, 0, 50);
await label.TranslateTo(-5, 0, 50);
await label.TranslateTo(0, 0, 50);
Watch out: TranslateTo moves the visual rendering, but the touch/hit-test area stays at the original layout position. If you translate an element far from its spot, users won't be able to tap on it. For permanent repositioning, adjust layout margins or use absolute positioning instead.
ScaleTo and RelScaleTo — Size Animations
ScaleTo animates the uniform Scale property. You can also use ScaleXTo and ScaleYTo for directional scaling. RelScaleTo adds a relative increment to the current scale — handy when you don't know (or care about) the current value.
// Button press feedback
await button.ScaleTo(0.9, 100, Easing.CubicIn);
await button.ScaleTo(1.0, 100, Easing.CubicOut);
// Attention pulse
await icon.ScaleTo(1.3, 200);
await icon.ScaleTo(1.0, 200);
RotateTo and RelRotateTo — Rotation Animations
RotateTo animates Rotation to an absolute angle. RelRotateTo adds a relative rotation. And if you want that 3D card-flip look, RotateXTo and RotateYTo animate perspective rotations.
// Spin a refresh icon
await refreshIcon.RotateTo(360, 800, Easing.Linear);
refreshIcon.Rotation = 0; // Reset for next spin
// 3D card flip
await card.RotateYTo(90, 300);
// Swap content here
await card.RotateYTo(0, 300);
Easing Functions: Controlling Animation Curves
Every built-in animation method accepts an optional Easing parameter as its last argument. Easing functions control how the animation speeds up and slows down, and they make a huge difference in how natural (or mechanical) the motion feels.
I'd say picking the right easing function matters more than getting the duration exactly right.
Available Easing Functions in .NET MAUI
| Easing Function | Behavior | Best For |
|---|---|---|
Linear | Constant speed | Progress bars, continuous rotation |
SinIn | Slow start, accelerates | Elements entering the screen |
SinOut | Fast start, decelerates | Elements coming to rest |
SinInOut | Slow start and end | Natural back-and-forth motion |
CubicIn | Aggressive slow start | Heavy objects starting to move |
CubicOut | Aggressive deceleration | Sliding panels settling into place |
CubicInOut | Smooth S-curve | Page transitions, dialogs |
BounceIn | Bounces at the start | Attention-grabbing entrances |
BounceOut | Bounces at the end | Dropping elements into place |
SpringIn | Overshoots then settles (start) | Elastic pull effects |
SpringOut | Overshoots then settles (end) | Snappy toggles, switches |
// Bounce a notification badge into view
badge.Scale = 0;
badge.IsVisible = true;
await badge.ScaleTo(1.0, 500, Easing.BounceOut);
// Spring-loaded drawer
await drawer.TranslateTo(0, 0, 400, Easing.SpringOut);
You can also create custom easing functions by passing a Func<double, double> to the Easing constructor. Input and output values range from 0.0 to 1.0.
var customEase = new Easing(t => t * t * t); // Cubic-in equivalent
await box.FadeTo(1, 600, customEase);
Sequential and Concurrent Animations
Since all ViewExtensions methods return Task, you can compose them into sophisticated sequences using normal async/await patterns. No special animation framework gymnastics required.
Sequential Animations
Just await each animation in order. The next one starts only after the previous one finishes.
// Entry animation: fade in, then slide up
myView.Opacity = 0;
myView.TranslationY = 50;
await myView.FadeTo(1, 300);
await myView.TranslateTo(0, 0, 300, Easing.CubicOut);
Concurrent Animations with Task.WhenAll
Use Task.WhenAll to run multiple animations at the same time. This is where things start looking really polished — a simultaneous fade and slide feels way better than doing them one after the other.
// Fade and slide simultaneously
myView.Opacity = 0;
myView.TranslationY = 50;
await Task.WhenAll(
myView.FadeTo(1, 400),
myView.TranslateTo(0, 0, 400, Easing.CubicOut)
);
// Staggered list item entrance
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
item.Opacity = 0;
item.TranslationX = -30;
_ = Task.Delay(i * 80).ContinueWith(async _ =>
{
await Task.WhenAll(
item.FadeTo(1, 300),
item.TranslateTo(0, 0, 300, Easing.CubicOut)
);
}, TaskScheduler.FromCurrentSynchronizationContext());
}
Cancelling Animations
Call ViewExtensions.CancelAnimations(element) to stop all running animations on a specific element. You'll want to do this when a user navigates away mid-animation or taps something again before the current animation finishes.
ViewExtensions.CancelAnimations(myView);
// Or as an extension method:
myView.CancelAnimations();
Custom Animations with the Animation Class
When ViewExtensions methods aren't flexible enough — and eventually they won't be — you can build frame-by-frame animations using the Animation class. This gives you full control over timing, child animations, and the ability to animate any property you want, not just the ones ViewExtensions exposes.
Creating a Basic Custom Animation
var animation = new Animation(
callback: v => myBox.BackgroundColor = Color.FromHsla(v, 1, 0.5),
start: 0,
end: 1,
easing: Easing.Linear
);
animation.Commit(
owner: this,
name: "ColorCycleAnimation",
length: 2000,
repeat: () => true // Loop forever
);
The Commit method attaches the animation to an owner (usually the page or view) and kicks it off. Here are the key parameters:
- owner — the
IAnimatablethat owns this animation (pages and views implement this interface) - name — a unique string identifier; reusing a name on the same owner cancels the previous animation
- length — duration in milliseconds
- repeat — a
Func<bool>that returnstrueto keep looping - finished — callback invoked when the animation ends
Child Animations for Storyboard Effects
Here's where the Animation class really shines. It supports child animations, each running during a specific fraction of the parent's total duration. Think of it like a storyboard — you define phases that play out in sequence within a single animation commit.
var parent = new Animation();
// Phase 1 (0% - 40%): Scale up
parent.Add(0, 0.4, new Animation(
v => myView.Scale = v, 1, 1.5, Easing.CubicOut));
// Phase 2 (40% - 70%): Rotate
parent.Add(0.4, 0.7, new Animation(
v => myView.Rotation = v, 0, 360, Easing.Linear));
// Phase 3 (70% - 100%): Scale back down
parent.Add(0.7, 1.0, new Animation(
v => myView.Scale = v, 1.5, 1, Easing.CubicIn));
parent.Commit(this, "StoryboardAnimation", length: 2000);
Child animations can overlap in their time ranges to create blended effects, too. For instance, starting a fade-out at 60% while a scale-down begins at 50% produces a smooth combined exit that looks really natural.
Stopping Custom Animations
// Stop a specific animation by name
this.AbortAnimation("ColorCycleAnimation");
Lottie Animations with SkiaSharp
For complex vector animations — loading spinners, success checkmarks, onboarding illustrations — Lottie is the way to go. These are JSON-based animations exported from Adobe After Effects via the Bodymovin plugin. They're lightweight, resolution-independent, and look crisp on any screen size.
I've used Lottie in a few production apps now, and it's honestly one of the best ways to add visual polish without having your designer redo assets for every resolution.
Setting Up SkiaSharp Lottie in .NET MAUI
The most established option is SkiaSharp.Extended.UI.Maui. Here's how to get it running.
Step 1: Install the NuGet package.
dotnet add package SkiaSharp.Extended.UI.Maui
Step 2: Register SkiaSharp in MauiProgram.cs.
using SkiaSharp.Views.Maui.Controls.Hosting;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseSkiaSharp();
return builder.Build();
}
}
Step 3: Add your .json Lottie file to Resources/Raw in your project. You can grab free animations from LottieFiles.
Step 4: Use SKLottieView in your XAML.
<ContentPage
xmlns:skia="clr-namespace:SkiaSharp.Extended.UI.Controls;assembly=SkiaSharp.Extended.UI"
...>
<skia:SKLottieView
Source="loading_spinner.json"
RepeatCount="-1"
RepeatMode="Restart"
HeightRequest="200"
WidthRequest="200"
HorizontalOptions="Center" />
</ContentPage>
Key SKLottieView Properties
| Property | Type | Description |
|---|---|---|
Source | SKLottieImageSource | Path to the JSON file in Resources/Raw or a URI |
RepeatCount | int | Number of loops. -1 for infinite |
RepeatMode | SKLottieRepeatMode | Restart or Reverse |
Progress | TimeSpan | Current playback position |
Duration | TimeSpan | Total animation duration |
IsAnimationEnabled | bool | Play or pause the animation |
IsComplete | bool | Whether playback has finished |
Loading Lottie from a URL
You can also stream Lottie animations from a remote server — useful for A/B testing different animations or updating them without shipping a new app version.
<skia:SKLottieView
Source="https://example.com/animations/success.json"
RepeatCount="0"
HeightRequest="150"
WidthRequest="150" />
Alternative: MPowerKit.Lottie
If you need native Lottie rendering (using lottie-android and lottie-ios under the hood), check out MPowerKit.Lottie. It provides hardware acceleration and tint color support, which can matter for complex animations on lower-end devices.
dotnet add package MPowerKit.Lottie
Animating Page Transitions
Here's the thing — .NET MAUI doesn't ship with a built-in API for custom page transition animations yet. There's a shared-element transitions spec being tracked on the MAUI GitHub repo, but it's not landed. In the meantime, you've got two solid workarounds.
Approach 1: Manual Transitions with OnAppearing
Override OnAppearing in your page and animate content elements as the page loads. It's straightforward and gives you plenty of control.
public partial class DetailPage : ContentPage
{
protected override async void OnAppearing()
{
base.OnAppearing();
// Start elements off-screen / invisible
HeaderImage.Opacity = 0;
HeaderImage.TranslationY = -30;
TitleLabel.Opacity = 0;
ContentStack.Opacity = 0;
ContentStack.TranslationY = 40;
// Staggered entrance
await HeaderImage.FadeTo(1, 300);
await HeaderImage.TranslateTo(0, 0, 300, Easing.CubicOut);
await TitleLabel.FadeTo(1, 200);
await Task.WhenAll(
ContentStack.FadeTo(1, 400),
ContentStack.TranslateTo(0, 0, 400, Easing.CubicOut)
);
}
}
Approach 2: Platform-Specific Transition Overrides
On Android and Windows, you can override the default navigation transition at the platform level. This takes a bit more work, but the results feel more native.
Android — Custom Activity Transitions:
// In Platforms/Android/MainActivity.cs
using Android.App;
using Android.OS;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true)]
public class MainActivity : MauiAppCompatActivity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
base.OnCreate(savedInstanceState);
// Override default transition
if (Build.VERSION.SdkInt >= BuildVersionCodes.Upside_down_cake)
{
OverrideActivityTransition(
OverrideTransition.Open,
Android.Resource.Animation.FadeIn,
Android.Resource.Animation.FadeOut);
}
}
}
Windows — WinUI NavigationTransition:
<!-- In Platforms/Windows/App.xaml -->
<maui:MauiWinUIApplication.Resources>
<Style TargetType="Frame">
<Setter Property="ContentTransitions">
<Setter.Value>
<TransitionCollection>
<EntranceThemeTransition />
</TransitionCollection>
</Setter.Value>
</Setter>
</Style>
</maui:MauiWinUIApplication.Resources>
CommunityToolkit Animation Behaviors
The .NET MAUI CommunityToolkit includes an AnimationBehavior that lets you attach animations to any visual element right in XAML — no code-behind needed. The animation triggers on tap or via a command binding.
Installation
dotnet add package CommunityToolkit.Maui
Using AnimationBehavior in XAML
<ContentPage
xmlns:toolkit="http://schemas.microsoft.com/dotnet/2022/maui/toolkit"
...>
<Image Source="star.png">
<Image.Behaviors>
<toolkit:AnimationBehavior>
<toolkit:AnimationBehavior.AnimationType>
<toolkit:FadeAnimation Opacity="0.5" />
</toolkit:AnimationBehavior.AnimationType>
</toolkit:AnimationBehavior>
</Image.Behaviors>
</Image>
</ContentPage>
Creating Custom CommunityToolkit Animations
You can build reusable animations by inheriting from BaseAnimation. The toolkit gives you Length and Easing as bindable properties out of the box.
using CommunityToolkit.Maui.Animations;
public class ShakeAnimation : BaseAnimation
{
public override async Task Animate(VisualElement view, CancellationToken token = default)
{
await view.TranslateTo(-15, 0, Length / 4, Easing);
await view.TranslateTo(15, 0, Length / 4, Easing);
await view.TranslateTo(-10, 0, Length / 4, Easing);
await view.TranslateTo(0, 0, Length / 4, Easing);
}
}
// XAML usage:
// <toolkit:AnimationBehavior>
// <toolkit:AnimationBehavior.AnimationType>
// <local:ShakeAnimation Length="400" Easing="{x:Static Easing.CubicInOut}" />
// </toolkit:AnimationBehavior.AnimationType>
// </toolkit:AnimationBehavior>
Practical UI Animation Patterns
So, let's get into some real-world patterns. These are ready-to-use snippets for common mobile UI scenarios that I keep coming back to across projects.
Animated FAB (Floating Action Button) Menu
private bool _isMenuOpen = false;
private async Task ToggleFabMenu()
{
_isMenuOpen = !_isMenuOpen;
if (_isMenuOpen)
{
// Rotate the main FAB
await MainFab.RotateTo(45, 200, Easing.CubicOut);
// Fan out sub-buttons
await Task.WhenAll(
SubFab1.TranslateTo(0, -70, 250, Easing.SpringOut),
SubFab1.FadeTo(1, 200),
SubFab2.TranslateTo(-60, -40, 250, Easing.SpringOut),
SubFab2.FadeTo(1, 200)
);
}
else
{
await Task.WhenAll(
SubFab1.TranslateTo(0, 0, 200, Easing.CubicIn),
SubFab1.FadeTo(0, 150),
SubFab2.TranslateTo(0, 0, 200, Easing.CubicIn),
SubFab2.FadeTo(0, 150)
);
await MainFab.RotateTo(0, 200, Easing.CubicIn);
}
}
Skeleton Loading Placeholder
This one's great for giving users visual feedback while data loads. The pulsing opacity effect is subtle but it signals that something is happening.
private void StartSkeletonAnimation(BoxView placeholder)
{
var animation = new Animation(
v => placeholder.Opacity = v,
0.3, 1.0, Easing.SinInOut);
animation.Commit(placeholder, "SkeletonPulse",
length: 1000,
repeat: () => true,
finished: (v, c) => placeholder.Opacity = 0.3);
}
// Stop when data loads
private void StopSkeletonAnimation(BoxView placeholder)
{
placeholder.AbortAnimation("SkeletonPulse");
placeholder.Opacity = 1;
}
Pull-to-Refresh Bounce
private async Task AnimateRefreshComplete(VisualElement indicator)
{
await indicator.ScaleTo(1.2, 150, Easing.CubicOut);
await indicator.ScaleTo(0, 200, Easing.CubicIn);
indicator.IsVisible = false;
indicator.Scale = 1;
}
Animation Performance Tips
Animations run on the UI thread by default, and poorly optimized ones will cause dropped frames — especially on lower-end Android devices. Here's what I've learned the hard way.
- Prefer transforms over layout changes. Animating
TranslationX,TranslationY,Scale,Rotation, andOpacityis GPU-accelerated on most platforms. AnimatingWidthRequest,HeightRequest, orMargintriggers layout recalculations every single frame. Don't do that. - Keep durations short. Most UI animations should land between 200–400ms. Anything past 500ms starts feeling sluggish. Only decorative Lottie animations should run longer.
- Cancel animations before starting new ones. If a user rapidly taps a button, overlapping animations produce weird, unpredictable results. Always call
CancelAnimations()before re-animating. - Respect system animation settings. On Android, if the user has disabled animations (via Developer Options or Accessibility), .NET MAUI animations will jump to their final state instantly. Don't fight this — it's an accessibility preference.
- Stagger list animations. Rather than animating 20 elements at once, add short delays (50–100ms per item). It looks better and doesn't hammer the UI thread.
- Use
IsVisibleinstead of opacity 0. An element withOpacity = 0still participates in layout and hit testing. If you're hiding something for good after an animation, setIsVisible = falseonce the fade completes.
Debugging Animations
When animations aren't behaving right, here's how to track down the problem:
- Slow down the duration. Set it to 3000–5000ms so you can actually see what's happening frame by frame. You'd be surprised how often this reveals the issue immediately.
- Log callback values. In custom
Animationcallbacks, print the intermediate values to debug output to verify the interpolation range is what you expect. - Check the initial state. This one gets me more often than I'd like to admit — animating to a value that's already the current value. Calling
FadeTo(1)on a fully opaque element does exactly nothing. - Verify the owner is alive. If you
Commitan animation to a page that's already been popped from the navigation stack, the animation gets collected and never runs.
Frequently Asked Questions
How do I loop an animation indefinitely in .NET MAUI?
It depends on the approach. For ViewExtensions, wrap the animation in a while loop with a cancellation token. For the Animation class, pass repeat: () => true to Commit. For Lottie, just set RepeatCount="-1" on the SKLottieView.
Can I animate custom properties like BackgroundColor?
Absolutely. The Animation class accepts any callback, so you can interpolate colors, corner radii, or whatever else you need. Use Color.FromHsla or Color.FromRgba with interpolated values for smooth color transitions.
Why do my animations not play on Android?
Nine times out of ten, the device has animations disabled. Go to Settings > Developer Options > Window animation scale and check if it's set to "Animation off." .NET MAUI respects these system-level settings, and your animations will jump straight to the end state.
What is the difference between SkiaSharp Lottie and MPowerKit Lottie?
SkiaSharp Lottie renders everything through the Skia graphics engine in a cross-platform managed layer. MPowerKit Lottie wraps the native lottie-android and lottie-ios libraries, giving you hardware acceleration and closer parity with native Lottie features. SkiaSharp is more portable; MPowerKit tends to perform better for complex animations on mobile.
How do I create page transition animations in .NET MAUI?
There's no built-in cross-platform API for this yet (though a spec is tracked on the .NET MAUI GitHub repo). Your best options right now are animating content elements in the OnAppearing override, using platform-specific transition overrides in the Platforms folder, or trying the CustomShellMaui community library for configurable Shell navigation animations.