Animation and visual states
Storyboards and property animation, easing, triggers and VisualStateManager, transitions, and how to respect a user who asked for less motion.
Storyboards and properties
<Border x:Name="Card" Width="200" Height="80" Background="#2F6FED">
<Border.Triggers>
<EventTrigger RoutedEvent="MouseEnter">
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity"
To="0.85" Duration="0:0:0.15" />
<DoubleAnimation Storyboard.TargetProperty="Width"
To="220" Duration="0:0:0.2">
<DoubleAnimation.EasingFunction>
<CubicEase EasingMode="EaseOut" />
</DoubleAnimation.EasingFunction>
</DoubleAnimation>
</Storyboard>
</BeginStoryboard>
</EventTrigger>
</Border.Triggers>
</Border>- Animate only properties that can be animated cheaply:
Opacity,RenderTransformandEffectare composited;Width,HeightandMarginforce a layout pass on every frame. Durationis required on most animations - omitting it on aDoubleAnimationthat targets a non-zero start makes the effect invisible.- Set
RenderTransformOriginto scale or rotate around the centre instead of the top-left corner. - Use
FillBehavior="Stop"when the animated value must not persist after the storyboard ends.
VisualStateManager
<ControlTemplate TargetType="{x:Type local:StatusButton}">
<Border x:Name="Root" Background="{TemplateBinding Background}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="MouseOver">
<Storyboard>
<ColorAnimation Storyboard.TargetName="Root"
Storyboard.TargetProperty="(Border.Background).(SolidColorBrush.Color)"
To="#1F5ED0" Duration="0:0:0.12" />
</Storyboard>
</VisualState>
<VisualState x:Name="Disabled">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="Root" Storyboard.TargetProperty="Opacity" To="0.4" Duration="0" />
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter />
</Border>
</ControlTemplate>// a control drives its own states from code
protected override void OnIsEnabledChanged(DependencyPropertyChangedEventArgs e)
{
base.OnIsEnabledChanged(e);
VisualStateManager.GoToState(this, IsEnabled ? "Normal" : "Disabled", useTransitions: true);
}States are the right model for a reusable control: the control decides which state it is in, and the template decides what that state looks like. Triggers scattered through a template become unmanageable once there are more than a handful.
Reduced motion and perceived speed
// read the OS preference once at startup and keep the whole app consistent
private static bool ReducedMotion =>
SystemParameters.ClientAreaAnimation == false ||
SystemParameters.MenuAnimation == false;
if (!ReducedMotion)
Storyboard.Begin(this);| Property | Layout cost | Use it for |
|---|---|---|
Opacity | None - composited | Fades and cross-fades |
RenderTransform | None - composited | Slide, scale, rotate |
Width / Height | Layout pass per frame | Avoid in animation |
Margin / Padding | Layout pass per frame | Avoid in animation |
Visibility | Layout and render tree change | Not animatable - toggle, do not animate |
⚠️
Animation should explain a change, not decorate it. A 120-200 ms transition tells the user something moved; a 600 ms flourish on every hover makes the application feel slower than it is. Anything longer than 300 ms should have a reason.
FAQ
Why did my animation not run after the first time?
A completed storyboard holds the final value and the property is no longer at its base value. Use
FillBehavior="Stop", or remove the storyboard explicitly before starting it again.Can I animate a bound value?
You can, but the animation wins over the binding while it runs and the setter is not called. Animate a separate property such as
Opacity and leave the bound value alone.Related
2D graphics, shapes and transforms Resources, styles, templates and themes
Last refreshed 2026-09-18.