Micro
Attention and ambient keyframe animations that play on an element directly, rather than riding the discrete presence-state attributes: a validation shake, a live-status pulse, a shimmer sweep, a focus-glow ring. One preset table (MicroPresets) feeds both the generated CSS classes and the runtime handle, so the two tiers animate identically.
@* Runtime tier: one-shot shake on validation, a start/stop pulse loop. *@
<div @ref="_shakeEl">
<input @bind="_email" />
<button @onclick="SubmitEmail">Submit</button>
</div>
<span @ref="_pulseEl" class="h-3 w-3 rounded-full bg-primary"></span>
@* CSS tier: no JS at all. *@
<div class="motion-shimmer h-16 w-40 rounded-md bg-muted"></div>
<div class="motion-focus-glow h-16 w-40 rounded-md border border-border"></div>The four presets
Shake is one-shot: trigger it on an event and it plays once. The other three loop until stopped. Values are inky and restrained by design: distances in a few pixels, colours driven by neutral CSS variables (one-ink, no hue).
| Preset | Feel | Shot | Duration / easing | CSS vars |
|---|---|---|---|---|
| Shake | A quick decaying left/right nudge that ends exactly at identity. | One-shot | 450ms, cubic-bezier(0.36, 0.07, 0.19, 0.97) | - |
| Pulse | A gentle breathing of opacity and scale, for a live-status dot or similar. | Loop | 1600ms, ease-in-out | - |
| Shimmer | A translucent one-ink highlight sweeping across the element via background-position. | Loop | 1600ms, linear | --navius-motion-shimmer-tint |
| FocusGlow | A hairline ring that pulses outward and fades. | Loop | 1800ms, ease-out | --navius-motion-glow-color, --navius-motion-glow-color-fade, --navius-motion-glow-ring |
Shimmer and FocusGlow also carry a static base style (the gradient surface, the resting hairline ring) that is present whether or not the animation is playing, which is what their reduced-motion fallback rests on.
CSS tier
Add the class and it animates on its own. Shake replays only when the class is re-added (remount it, or use the runtime tier to trigger it on demand); the loops start immediately and run until removed.
| Class | Keyframes | Runs |
|---|---|---|
| .motion-shake | navius-shake | Once, on class add. |
| .motion-pulse | navius-pulse | Infinite. |
| .motion-shimmer | navius-shimmer | Infinite. |
| .motion-focus-glow | navius-focus-glow | Infinite. |
Motion has a matching splat for each, for zero-wrapper usage with @attributes.
| Prop | Type | Default |
|---|---|---|
| Motion.Shake() | IReadOnlyDictionary<string, object> | motion-shake |
| Motion.Pulse() | IReadOnlyDictionary<string, object> | motion-pulse |
| Motion.Shimmer() | IReadOnlyDictionary<string, object> | motion-shimmer |
| Motion.FocusGlow() | IReadOnlyDictionary<string, object> | motion-focus-glow |
@* The class runs the animation on its own. *@
<div class="motion-shimmer h-16 w-40 rounded-md bg-muted"></div>
@* Or splat the helper (it owns the class attribute). *@
<div @attributes="Motion.FocusGlow()"></div>Runtime tier
Bind CreateMicroAsync to get a MotionMicro handle: nothing runs until you call PlayAsync, and a looped preset keeps going until StopAsync. MotionPrograms.Micro converts a preset's keyframes to the WAAPI frames the executor plays, so the runtime animates with the exact same curve as the generated class.
@inject IJSRuntime JS
var interop = new MotionJsInterop(JS);
_shake = await interop.CreateMicroAsync(_shakeEl, MotionPrograms.Micro(MicroPresets.Shake));
_pulse = await interop.CreateMicroAsync(_pulseEl, MotionPrograms.Micro(MicroPresets.Pulse));
// One-shot, triggered on a real event:
await _shake.PlayAsync();
// Loop, started once and stopped later:
await _pulse.PlayAsync();
await _pulse.StopAsync();
// Later: await _shake.DisposeAsync(); await _pulse.DisposeAsync();MotionJsInterop
Bind a micro preset to an element, built with MotionPrograms.Micro. Dispose the handle to stop it and release the JS reference.
| Prop | Type | Default |
|---|---|---|
| CreateMicroAsync(ElementReference element, MicroOptions options) | Task<MotionMicro> | - |
| MotionPrograms.Micro(MicroPreset preset) | MicroOptions | - |
MotionMicro
The live binding. PlayAsync starts (or restarts) the animation; StopAsync cancels a running loop and returns the element to its natural style.
| Prop | Type | Default |
|---|---|---|
| PlayAsync() | Task | Plays once (one-shot) or starts the loop (looping). |
| StopAsync() | Task | Cancels a running loop; the element returns to its natural style. |
| DisposeAsync() | ValueTask | Stops the animation and releases the JS handle. |
MicroOptions
Serialized to the executor (camelCase). Build it from a preset with MotionPrograms.Micro rather than by hand.
| Prop | Type | Default |
|---|---|---|
| Keyframes | IReadOnlyList<IReadOnlyDictionary<string, object>> | - |
| DurationMs | double | - |
| Easing | string | "ease-in-out" |
| Loop | bool | false |
| ReduceMotion | string | "user" |
Reduced motion
Every micro preset declares its own MicroReduce behaviour, honoured by both tiers under prefers-reduced-motion: reduce (CSS tier) or ReduceMotion (runtime tier, default "user").
| Preset | MicroReduce | What is left |
|---|---|---|
| Shake | Collapse | Nothing plays, it never moves at all (transform-only, no base style). |
| Pulse | OpacityOnly | The opacity beat continues (scale dropped); the live status still reads, non-vestibular. |
| Shimmer | Collapse | The sweep stops, but the gradient base style remains: a static placeholder, not a blank box. |
| FocusGlow | Collapse | The pulse stops, but the base hairline ring remains: the emphasis is kept, just not animated. |
MicroReduce.Collapse means animation: none in the generated CSS: the element rests at its base style, which is empty for Shake and the gradient/ring for Shimmer and FocusGlow. MicroReduce.OpacityOnly swaps to a second, opacity-only @keyframes block generated alongside the main one, so the runtime tier strips every non-opacity property instead of switching keyframe sets, and reaches the same result.