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, RenderTransform and Effect are composited; Width, Height and Margin force a layout pass on every frame.
  • Duration is required on most animations - omitting it on a DoubleAnimation that targets a non-zero start makes the effect invisible.
  • Set RenderTransformOrigin to 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);
PropertyLayout costUse it for
OpacityNone - compositedFades and cross-fades
RenderTransformNone - compositedSlide, scale, rotate
Width / HeightLayout pass per frameAvoid in animation
Margin / PaddingLayout pass per frameAvoid in animation
VisibilityLayout and render tree changeNot 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.

2D graphics, shapes and transforms Resources, styles, templates and themes

Last refreshed 2026-09-18.