Height Animation
An overflow-clipped container that WAAPI-tweens its height so a collapse, an expand, or a content-size change glides instead of jumping. The physics are solved and baked to a linear() easing in C#; JS only executes.
@* Collapse/expand between 0 and the natural height, on a baked spring. *@
<NaviusHeightAnimation Expanded="_open" Spring="Spring.Default" class="rounded-md border">
<div class="p-4">@* content *@</div>
</NaviusHeightAnimation>
<button @onclick="() => _open = !_open">Toggle</button>
@code { private bool _open = true; }The component
NaviusHeightAnimation renders one overflow-clipped <div> (a single element, per ADR-0003, so there is no asChild) around an inner measured wrapper, and tweens the outer height to match. Splat your layout classes onto it; the outer element carries data-navius-height-animation and the inner carries data-navius-height-animation-content.
@using Navius.Motion
@* Always track the content size (never collapses): *@
<NaviusHeightAnimation class="..."> ... </NaviusHeightAnimation>
@* Collapse/expand on a timed tween instead of a spring: *@
<NaviusHeightAnimation Expanded="_open" Duration="200" Easing="ease-out" class="..."> ... </NaviusHeightAnimation>NaviusHeightAnimation
An auto-height container. Expanded distinguishes the two modes; Duration/Easing time the tween, or pass a Spring to bake it onto a linear() curve.
| Prop | Type | Default |
|---|---|---|
| Expanded | bool? | null (track) |
| Duration | double? | 300 |
| Easing | string? | "ease" |
| Spring | Spring? | - |
| ReduceMotion | string | "user" |
| ChildContent | RenderFragment? | - |
| Attributes | IDictionary<string, object>? (splat) | - |
Two modes
One nullable flag distinguishes them. Expanded = null (the default) always tracks the content's natural size and never collapses, which suits a panel whose contents grow. Expanded = true / false is a collapse/expand between 0 and the natural height, and content-tracking still applies while expanded.
Collapsed content stays mounted and in the tab order (visually hidden via overflow:hidden + height:0); this does not manage inert / hidden / tabindex. For an accessibility-gated collapse use Collapsible (and drop this on its panel for the height glide).
Timing the tween
With no timing arguments the tween uses the calm defaults: 300ms, ease. Pass a Spring and it bakes to a linear() easing plus its settle duration; Duration and Easing override either dimension independently. Under prefers-reduced-motion (or ReduceMotion="always") the tween is skipped and the end state applies instantly; ReduceMotion="never" opts one container out.
Imperative runtime
The component is a thin wrapper over the interop. Bind it yourself when you need the handle: CreateHeightAnimationAsync attaches a ResizeObserver on the content, SetExpandedAsync tweens between collapsed (0) and open (natural), RemeasureAsync re-checks the content size after a change the observer could miss, and disposing disconnects the observer and cancels any in-flight tween.
@inject IJSRuntime JS
var interop = new MotionJsInterop(JS);
var options = MotionPrograms.HeightAnimation(Spring.Default, expanded: true);
_handle = await interop.CreateHeightAnimationAsync(_outer, _content, options);
// Collapse / expand:
await _handle.SetExpandedAsync(false);
// After a content change the ResizeObserver could miss:
await _handle.RemeasureAsync();
// Later: await _handle.DisposeAsync();MotionJsInterop
Bridge to navius-motion.js. CreateHeightAnimationAsync returns a HeightAnimationMotion; build its options from a spring (or raw timing) with MotionPrograms.HeightAnimation.
| Prop | Type | Default |
|---|---|---|
| CreateHeightAnimationAsync(ElementReference element, ElementReference content, HeightAnimationOptions options) | Task<HeightAnimationMotion> | - |
| MotionPrograms.HeightAnimation(Spring? spring = null, double? durationMs = null, string? easing = null, string reduceMotion = "user", bool? expanded = null) | HeightAnimationOptions | - |
HeightAnimationOptions
Serialized to the executor. Easing accepts a baked linear() spring string; Expanded is the initial mode.
| Prop | Type | Default |
|---|---|---|
| DurationMs | double | 300 |
| Easing | string | "ease" |
| ReduceMotion | string | "user" |
| Expanded | bool? | null |
HeightAnimationMotion
The live binding. SetExpandedAsync tweens between collapsed and open; RemeasureAsync re-tracks the content; dispose to disconnect.
| Prop | Type | Default |
|---|---|---|
| SetExpandedAsync(bool? expanded) | Task | - |
| RemeasureAsync() | Task | - |
| DisposeAsync() | ValueTask | - |