navius

Orchestration

Coordinating motion across elements and time: reveal on scroll, stagger a group, compose an imperative timeline, slide a selection marker, and cross-fade same-document navigations. Every curve is still a C#-solved spring baked to linear(); JS only executes.

In view

Reveal an element the first time it scrolls into view. The CSS tier does the animating: each .motion-in-view-* class starts the element hidden and transitions it in when [data-in-view] is set. An IntersectionObserver from CreateInViewAsync sets that attribute, so the reveal itself is zero per-frame JS. Splat the class with Motion.InView, then attach the observer.

1
2
3
4
5
6

There is one .motion-in-view-* class per preset (fade, fade-up, zoom, pop, the four slides), matching the presence set. Passing a Stagger to the options makes the same intersection fan the attribute out to the direct children, with each child's delay set up front, so a whole group reveals in sequence off one observer.

Splat helper (CSS tier): the .motion-in-view-<preset> class plus an optional delay var. Attach the observer separately.

Prop Type Default
Motion.InView(Preset preset, double delayMs = 0) IReadOnlyDictionary<string, object> -

Sets data-in-view while the element intersects. The callback overload also reports coarse enter/leave edges via OnInView(bool).

Prop Type Default
CreateInViewAsync(ElementReference element, InViewOptions options) Task<InViewMotion> -
CreateInViewAsync<T>(ElementReference element, InViewOptions options, DotNetObjectReference<T> callback) Task<InViewMotion> -

Serialized to camelCase. Amount is the visibility threshold (0 = any pixel, 1 = fully visible); Once keeps data-in-view after the first entry; a non-null Stagger fans the reveal out to the children.

Prop Type Default
Amount double 0
Margin string "0px"
Once bool false
Stagger StaggerOptions? null

The live observer. RefreshAsync re-reads the child list after a staggered group's children change (a no-op without a stagger); dispose to disconnect.

Prop Type Default
RefreshAsync() Task -
DisposeAsync() ValueTask -

Stagger

A stagger is a per-child delay schedule written into --navius-motion-delay, which the enter and in-view classes already read. MotionStagger.Delays is the authoring surface (index-aligned delays in milliseconds); the same distance-from-anchor formula is duplicated in navius-motion.js and kept in lock-step by a test. Use CreateStaggerAsync to apply the vars to a group revealed by other means, or the Stagger option above when the in-view observer drives the reveal.

The From anchor sets who moves first. Center uses fractional distance for an even count (Motion's stagger semantics), so delays can be half-steps.

The authoring/spec surface for the delay schedule.

Prop Type Default
MotionStagger.Delays(int count, double stepMs, StaggerFrom from = StaggerFrom.First) double[] -
StaggerFrom.ToToken() string -

The anchor a stagger fans out from.

Prop Type Default
StaggerFrom.First - first child moves first
StaggerFrom.Last - last child moves first
StaggerFrom.Center - centre moves first (half-steps for an even count)

Serialized to camelCase. Build from a typed anchor with StaggerOptions.Of; pass to CreateStaggerAsync or as InViewOptions.Stagger.

Prop Type Default
Step double 50
From string "first"
StaggerOptions.Of(double step, StaggerFrom from) StaggerOptions -

Set the per-child delay vars from a schedule, with no in-view coupling. RefreshAsync re-reads the child list.

Prop Type Default
CreateStaggerAsync(ElementReference container, StaggerOptions options) Task<StaggerMotion> -
StaggerMotion.RefreshAsync() Task -

Sequences

MotionSequence is the imperative plane: a fluent builder that composes a multi-element timeline in C# and compiles it to a serialized program the executor plays. Springs bake to linear() at Build time and every segment's at offset resolves to an absolute start time, so the runtime just plays one WAAPI animation per segment on a shared clock. That makes Seek a single scrub with no per-frame loop.

0 ms

A segment's at is resolved against a running cursor (the end of the previous segment):

Prop Type Default
null - plays right after the previous segment
"+120" - starts 120ms after the previous segment
"-80" - overlaps the previous segment by 80ms
"<" - starts together with the previous segment
"400" - a bare number is an absolute start time
"label" - starts at a Label's marked time

Play resumes from the current position, and segments that already finished hold on their final frame rather than replaying (they are filled both ways). A plain resume would otherwise re-run the finished segments while the rest continue and desync the timeline. To replay from the top, call Stop (which rewinds every segment) and then Play.

Own the limits honestly: no per-frame or onUpdate callbacks, no nested sequences, no infinite iterations, and one segment targets one element (a selector's first match, scoped to the run root, or an @ref).

The builder. To adds a segment; Label marks the cursor for a later at; Build compiles to a MotionProgram.

Prop Type Default
To(string selector, MotionFrame[] keyframes, Spring spring, string? at = null) MotionSequence -
To(string selector, MotionFrame[] keyframes, double durationMs, string easing = "linear", string? at = null) MotionSequence -
To(ElementReference element, MotionFrame[] keyframes, Spring spring, string? at = null) MotionSequence -
To(ElementReference element, MotionFrame[] keyframes, double durationMs, string easing = "linear", string? at = null) MotionSequence -
Label(string name) MotionSequence -
Build() MotionProgram -
ReduceMotion string "user"

Play a compiled program. The root overload scopes selector targets to one element so several sequences never collide.

Prop Type Default
RunSequenceAsync(MotionProgram program) Task<MotionSequenceHandle> -
RunSequenceAsync(MotionProgram program, ElementReference root) Task<MotionSequenceHandle> -

Drives the whole timeline. DurationMs is the resolved total length; dispose to cancel every segment.

Prop Type Default
DurationMs double -
PlayAsync() Task -
PauseAsync() Task -
SeekAsync(double ms) Task -
StopAsync() Task -
WaitForFinishAsync() Task -
DisposeAsync() ValueTask -

Selection indicator

NaviusSelectionIndicator is a standalone spring-animated marker (the Tabs and NavigationMenu indicator math, generalized). Place it inside the container it should track, give that container position: relative, pass the container's @ref, and change the Active token whenever the active element changes. The marker slides to the element matching ActiveSelector: position rides a compositor transform and its size animates, so a pill or underline tracks items of differing size. It is resize-aware.

Renders one div (a single element, per ADR-0003); style it via splat. It self-wires once the Container ref is populated.

Prop Type Default
Container ElementReference (EditorRequired) -
Active object? -
ActiveSelector string "[data-active]"
Spring Spring Spring.Snappy
Axis string "both"
ReduceMotion string "user"
Attributes IDictionary<string, object>? (splat) -

To drive the marker yourself, the interop mirrors the component: CreateSelectionIndicatorAsync with options built by MotionPrograms.SelectionIndicator (spring baked to a linear() easing), then call UpdateAsync after the active element changes.

Serialized to camelCase. Axis is x, y or both; Easing is the baked spring the marker moves with. Build with MotionPrograms.SelectionIndicator.

Prop Type Default
CreateSelectionIndicatorAsync(ElementReference container, ElementReference indicator, SelectionIndicatorOptions options) Task<SelectionIndicatorMotion> -
MotionPrograms.SelectionIndicator(Spring? spring = null, string activeSelector = "[data-active]", string axis = "both", string reduceMotion = "user") SelectionIndicatorOptions -
SelectionIndicatorMotion.UpdateAsync() Task -

Page transitions

NaviusPageTransition is experimental: treat the API surface as unstable. It is scoped to same-document navigation only. Where the View Transition API is unsupported, or under prefers-reduced-motion, navigation falls back to instant with no visual effect.

Wrap the region whose content changes across navigations (typically the router region) in one NaviusPageTransition. It registers a NavigationManager location-changing handler and snapshots the change through document.startViewTransition. Because Blazor renders the new page through its own cycle rather than inside the transition callback, the handler blocks the navigation until the browser has captured the old snapshot, then resolves the transition once the new render has settled. Use one wrapper per app (a single in-flight transition).

Same-document transitions for the WASM router. Reduced or unsupported falls back to instant navigation.

Prop Type Default
ChildContent RenderFragment? -
ReduceMotion string "user"
SettleFrames int 2