loading
Note:
An extension method, not a service Like the Element APIs there is nothing to inject: Animate hangs off ElementReference itself and takes an IJSRuntime so it can reach the browser. Capture the element with @ref and animate it from OnAfterRenderAsync or an event handler.

Animate an element

Animate

Animate takes the keyframes to interpolate between and, optionally, the timing options. It returns an AnimationHandle immediately - the animation runs on the browser's own timeline from that moment, with no per-frame interop. Each keyframe is a dictionary of CSS property to value, exactly like the array you would pass to element.animate() in JavaScript.

Razor
<div @ref="box">...</div>

@code {
    private ElementReference box;

    private async Task Run()
    {
        await box.Animate(js,
            [
                new() { ["transform"] = "translateX(0)", ["opacity"] = "1" },
                new() { ["transform"] = "translateX(180px)", ["opacity"] = "0.4" },
                new() { ["transform"] = "translateX(0)", ["opacity"] = "1" },
            ],
            new AnimationOptions { Duration = 1200, Easing = "ease-in-out" });
    }
}
Live sample
Butil
animate output
Results will appear here when you interact with the samples.

Keyframes

AnimationKeyframes

AnimationKeyframes is a List of Dictionary<string, string>, so collection expressions and dictionary initializers build one inline. Every property must be spelled the way CSS spells it - kebab-case, not the camelCase form the JavaScript object syntax uses - and every keyframe should list the same properties so the engine has something to interpolate towards. Two keyframes mean 'from' and 'to'; more are distributed evenly across the duration.

Razor
@inject IJSRuntime js

<div @ref="box">...</div>
<button @onclick="Pulse">Animate</button>

@code {
    private ElementReference box;

    private async Task Pulse()
    {
        AnimationKeyframes pulse =
        [
            new() { ["transform"] = "scale(1)",    ["background-color"] = "#1276C6" },
            new() { ["transform"] = "scale(1.35)", ["background-color"] = "#FD7F36" },
            new() { ["transform"] = "scale(1)",    ["background-color"] = "#1276C6" },
        ];

        await box.Animate(js, pulse, new AnimationOptions { Duration = 900 });
    }
}
Live sample
Pulse
keyframes output
Results will appear here when you interact with the samples.

Timing options

AnimationOptions

AnimationOptions forwards the subset of KeyframeEffectOptions that survives the interop hop: Duration, Delay, EndDelay, Iterations, Easing, Direction, Fill and Composite. Set Iterations to double.PositiveInfinity to loop forever - Butil normalizes it on the JavaScript side, which JSON alone cannot express. Change the values below and re-run to feel the difference.

Razor
@inject IJSRuntime js

<div @ref="box">...</div>

@code {
    private ElementReference box;
    private AnimationHandle? handle;

    private async Task Run(AnimationKeyframes keyframes)
    {
        var options = new AnimationOptions
        {
            Duration = 1200,                       // ms
            Delay = 0,                             // ms before the first frame
            EndDelay = 0,                          // ms held after the last frame
            Iterations = double.PositiveInfinity,  // loop forever
            Easing = "cubic-bezier(0.33, 0, 0.67, 1)",
            Direction = "alternate",               // normal | reverse | alternate | alternate-reverse
            Fill = "forwards",                     // none | forwards | backwards | both | auto
            Composite = "replace",                 // replace | add | accumulate
        };

        handle = await box.Animate(js, keyframes, options);
    }
}
Live sample
Timing
Duration (ms)
Easing
Direction
Fill
timing options output
Results will appear here when you interact with the samples.

Control playback

Play / Pause / Reverse / Finish / Cancel / SetPlaybackRate

The returned AnimationHandle is a remote control for the running animation. Play and Pause stop and resume it in place, Reverse flips the direction from the current time, SetPlaybackRate speeds it up or slows it down (negative values run it backwards), Finish jumps to the end and Cancel removes it - snapping the element back to its unanimated styles. Start the looping animation below, then drive it.

Razor
@inject IJSRuntime js

<div @ref="box">...</div>

@code {
    private ElementReference box;
    private AnimationHandle? handle;

    private async Task Control(AnimationKeyframes keyframes)
    {
        handle = await box.Animate(js, keyframes,
            new AnimationOptions { Duration = 2000, Iterations = double.PositiveInfinity });

        await handle.Pause();
        await handle.SetPlaybackRate(0.25);   // quarter speed
        await handle.Play();
        await handle.Reverse();
        await handle.Finish();                // jump to the end
        await handle.Cancel();                // remove the animation entirely
    }
}
Live sample
Orbit
Playback rate: 1.00x
playback control output
Results will appear here when you interact with the samples.

Await completion

WhenFinished

WhenFinished awaits the animation's own finished promise, which is how you sequence work after an animation without guessing at a Task.Delay. It completes when the animation finishes or is cancelled, so it is safe to await in a teardown path - but an animation with infinite Iterations never finishes, and awaiting one would hang forever.

Razor
@inject IJSRuntime js

@if (isVisible)
{
    <div @ref="panel">...</div>
}

@code {
    private bool isVisible = true;
    private ElementReference panel;

    private static readonly AnimationKeyframes fadeOut =
    [
        new() { ["opacity"] = "1" },
        new() { ["opacity"] = "0" },
    ];

    private async Task Hide()
    {
        var handle = await panel.Animate(js, fadeOut,
            new AnimationOptions { Duration = 300, Fill = "forwards" });

        await handle.WhenFinished();

        // the element is now visually gone - safe to drop it from the render tree
        isVisible = false;
        StateHasChanged();
    }
}
Live sample
Steps
sequencing output
Results will appear here when you interact with the samples.

Dispose the handle

AnimationHandle.DisposeAsync

Every handle holds an entry in a JavaScript-side registry so the control methods can find the animation again. Disposing cancels the animation and drops that entry; without it, a Fill of forwards or both leaves the element pinned at its final frame and the registry entry alive for the lifetime of the page. Dispose is idempotent and swallows the usual teardown exceptions, so calling it from DisposeAsync during a circuit shutdown is safe.

Razor
@implements IAsyncDisposable

@code {
    private AnimationHandle? handle;

    public async ValueTask DisposeAsync()
    {
        if (handle is not null)
        {
            await handle.DisposeAsync();
        }
    }
}

Scroll-driven animations

AnimationOptions.Timeline

Passing a Timeline replaces the clock with a scroll position: a ScrollTimeline follows a scroller, a ViewTimeline follows an element's passage through the scrollport. Duration, Delay and EndDelay stop meaning anything and are not sent - RangeStart and RangeEnd say which part of the scroll the animation occupies, in the CSS syntax of the animation-range property.

Razor
@inject IJSRuntime js

<div @ref="_bar" style="transform-origin:left; height:3px; background:#1276C6"></div>

@code {
    private ElementReference _bar;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender is false) return;

        // A reading-progress bar, driven by the page's own scroll rather than by time - so nothing
        // here ticks, and nothing has to be cancelled when the reader stops scrolling.
        await _bar.Animate(js,
            [new() { ["transform"] = "scaleX(0)" }, new() { ["transform"] = "scaleX(1)" }],
            new AnimationOptions
            {
                Fill = "forwards",
                Timeline = new AnimationTimelineOptions { Type = "scroll", Axis = "block" }
            });

        // ... or fade an element in as it comes into view:
        // new AnimationTimelineOptions { Type = "view", RangeStart = "entry 0%", RangeEnd = "entry 100%" }
    }
}
Live sample
scroll timeline output
Results will appear here when you interact with the samples.

Keep the end state

CommitStyles / Persist

A filling animation keeps the element under the animation's control and beats every later style change; the browser also discards one once another supersedes it, taking its visual effect with it. CommitStyles writes the current computed values into the inline style so the state survives without the animation - the usual pairing is WhenFinished, CommitStyles, Cancel. Persist is the blunter alternative, and keeps the fight going.

Razor
@inject IJSRuntime js

<div @ref="_box">...</div>

@code {
    private ElementReference _box;

    private async Task Keep(AnimationKeyframes keyframes)
    {
        var handle = await _box.Animate(js, keyframes, new AnimationOptions { Fill = "forwards" });

        await handle.WhenFinished();

        // Writes the animated values into the element's own inline style, so cancelling the
        // animation afterwards leaves the element where it ended rather than snapping back.
        await handle.CommitStyles();
        await handle.Cancel();
    }
}
Live sample
Commit
commitStyles output
Results will appear here when you interact with the samples.

Everything animating on an element

GetAnimations / CancelAnimations

Unlike an AnimationHandle, which only knows about animations Butil started, this reports CSS animations and transitions too - which is how to answer 'is anything still moving here' before measuring, snapshotting or tearing an element down. CancelAnimations is the blunt companion: a CSS animation has no handle to cancel it through.

Razor
@inject IJSRuntime js

<div @ref="_box">...</div>

@code {
    private ElementReference _box;

    private async Task Inspect()
    {
        // Everything animating on the element, whoever started it - CSS transitions and animations
        // included, not only what this code created.
        var running = await _box.GetAnimations(js, subtree: true);
        if (running.Any(a => a.PlayState == "running")) await WaitForIt();

        var cancelled = await _box.CancelAnimations(js, subtree: true);
    }
}
Live sample
Running
getAnimations output
Results will appear here when you interact with the samples.
Note:
The animation runs in the browser, not over interopAnimate is a single interop call that hands the keyframes to the browser; every frame after that is computed by the compositor. That makes it dramatically cheaper than driving a style property from a .NET timer, and it is why animations stay smooth on Blazor Server, where every render round-trips over a circuit.
Warning:
Respect prefers-reduced-motion The Web Animations API does not consult the user's motion preference for you. Query it with Window.MatchMedia before starting anything decorative, and either skip the animation or collapse it to a very short duration when (prefers-reduced-motion: reduce) matches.

API reference

Member
Signature
Description
Animate
Task<AnimationHandle> Animate(this ElementReference element, IJSRuntime js, AnimationKeyframes keyframes, AnimationOptions? options = null, ElementReference? timelineSource = null)
Starts a Web Animation on the element and returns a handle for controlling it. Options default to a 1000ms linear animation; timelineSource is only used with a scroll-driven timeline.
AnimationKeyframes
class AnimationKeyframes : List<Dictionary<string, string>>
The keyframes to interpolate between: one dictionary of CSS property to value per frame.
AnimationOptions.Duration
double Duration = 1000
Total duration of one iteration, in milliseconds.
AnimationOptions.Delay
double Delay = 0
Milliseconds to wait before playback starts.
AnimationOptions.EndDelay
double EndDelay = 0
Milliseconds to hold after playback completes.
AnimationOptions.Iterations
double Iterations = 1
How many times to repeat. Use double.PositiveInfinity to loop forever.
AnimationOptions.Easing
string Easing = "linear"
Any CSS easing function, for example ease-in-out or cubic-bezier(0, 0, 0.2, 1).
AnimationOptions.Direction
string Direction = "normal"
normal, reverse, alternate or alternate-reverse.
AnimationOptions.Fill
string Fill = "none"
Which styles persist outside the active period: none, forwards, backwards, both or auto.
AnimationOptions.Composite
string Composite = "replace"
How the animation combines with the element's underlying value: replace, add or accumulate.
AnimationHandle.Play
ValueTask Play()
Resumes a paused animation.
AnimationHandle.Pause
ValueTask Pause()
Pauses the animation at its current time.
AnimationHandle.Reverse
ValueTask Reverse()
Reverses the playback direction from the current time.
AnimationHandle.Cancel
ValueTask Cancel()
Cancels the animation and removes its effect from the element.
AnimationHandle.Finish
ValueTask Finish()
Jumps to the end of the animation, applying Fill. No-ops when the engine rejects it, as it does for an infinitely-iterating animation.
AnimationHandle.WhenFinished
ValueTask WhenFinished()
Awaits the animation's finished promise. Returns when it finishes or is cancelled; never returns for an infinite animation.
AnimationOptions.Timeline
AnimationTimelineOptions? Timeline = null
Drives the animation from a scroll position rather than the clock. When set, Duration, Delay and EndDelay are not sent.
AnimationTimelineOptions.Type
string Type = "scroll"
scroll to follow a scroller's position, or view to follow an element's passage through the scrollport.
AnimationTimelineOptions.Axis
string Axis = "block"
Which axis drives it: block, inline, x or y.
AnimationTimelineOptions.RangeStart / RangeEnd
string RangeStart / RangeEnd
Where in the scroll range the animation runs, in the CSS syntax of animation-range - 'entry 0%', 'cover 20%'.
IsTimelineSupported
ValueTask<bool> IsTimelineSupported(this ElementReference element, IJSRuntime js)
True when the runtime implements ScrollTimeline / ViewTimeline. Where false, an animation asking for one runs on the ordinary clock instead.
GetAnimations
ValueTask<AnimationInfo[]> GetAnimations(this ElementReference element, IJSRuntime js, bool subtree = false)
Every animation currently affecting the element, including CSS animations and transitions the page never scripted.
CancelAnimations
ValueTask<int> CancelAnimations(this ElementReference element, IJSRuntime js, bool subtree = false)
Cancels every animation on the element, whoever started it. Returns how many.
AnimationHandle.SetPlaybackRate
ValueTask SetPlaybackRate(double rate)
Sets the playback rate: 1 is normal speed, 0.5 half speed, -1 reverse at normal speed.
AnimationHandle.CommitStyles
ValueTask<bool> CommitStyles()
Writes the animation's current computed values into the element's inline style, so the end state outlives the animation.
AnimationHandle.Persist
ValueTask<bool> Persist()
Opts the animation out of automatic removal. CommitStyles is usually the better answer.
AnimationHandle.DisposeAsync
ValueTask DisposeAsync()
Cancels the animation and releases the JavaScript-side registry entry. Idempotent and safe during teardown.
An unhandled error has occurred. Reload 🗙