Gestures
A press-and-hover micro-interaction pack. The CSS tier scales on press and lifts on hover with zero JavaScript; the runtime tier adds keyboard parity and coarse start/end callbacks to C#.
@* CSS tier: the class plus its variable, straight on the element. *@
<button class="motion-press" style="--navius-motion-press-scale: 0.94">Press me</button>
<button class="motion-hover" style="--navius-motion-hover-lift: 4">Hover me</button>CSS tier
Two classes, both timed with the snappy spring. .motion-press uses :active, which also covers keyboard activation on a real <button>. .motion-hover sits behind @media (hover: hover), so emulated touch hover never triggers a stuck lift.
| Class | Trigger | Variable (default) |
|---|---|---|
| .motion-press | :active | --navius-motion-press-scale (0.97) |
| .motion-hover | :hover, hover-capable pointers only | --navius-motion-hover-lift (2px) |
Splat helpers
Motion.Press and Motion.Hover return the class plus the custom-property override as a dictionary to splat with @attributes. Splat instead of a literal class (last wins), or write the class and the --navius-motion-* variable directly.
| Member | Returns | Effect |
|---|---|---|
| Motion.Press(double scale = 0.97) | IReadOnlyDictionary<string, object> | motion-press with the press-scale variable set. |
| Motion.Hover(double lift = 2) | IReadOnlyDictionary<string, object> | motion-hover with the hover-lift variable set (pixels). |
@* Or splat the helper (it owns the class attribute). *@
<button @attributes="Motion.Press(scale: 0.94)">Press me</button>
<button @attributes="Motion.Hover(lift: 4)">Hover me</button>Runtime gestures
For keyboard parity and event callbacks, bind CreateGestureAsync. The animation runs JS-side (one WAAPI tween at a time, snapshotting the live transform so changes flow smoothly); only coarse start/end edges cross to C#. Pass a DotNetObjectReference to receive them.
@inject IJSRuntime JS
var interop = new MotionJsInterop(JS);
var self = DotNetObjectReference.Create(this);
_gesture = await interop.CreateGestureAsync(
_element, "press", self, MotionPrograms.Gesture(pressScale: 0.92));
[JSInvokable] public void OnGestureStart(string kind) { /* press | hover */ }
[JSInvokable] public void OnGestureEnd(bool success) { /* activated / cancelled */ }
// Later: await _gesture.DisposeAsync();MotionJsInterop
Bind a press or hover gesture (kind is press or hover). The overload with a callback reports edges to it. Returns a MotionGesture; dispose it to remove the listeners.
| Prop | Type | Default |
|---|---|---|
| CreateGestureAsync(ElementReference element, string kind, GestureOptions options) | Task<MotionGesture> | - |
| CreateGestureAsync<T>(ElementReference element, string kind, DotNetObjectReference<T> callback, GestureOptions options) | Task<MotionGesture> | - |
| MotionPrograms.Gesture(Spring? spring = null, double pressScale = 0.97, double hoverLift = 2) | GestureOptions | - |
GestureOptions
Serialized to the executor. Timing is the baked spring the gesture animates with; build it with MotionPrograms.Gesture.
| Prop | Type | Default |
|---|---|---|
| DurationMs | double | - |
| Easing | string | - |
| PressScale | double | 0.97 |
| HoverLift | double | 2 |
| ReduceMotion | string | "user" |
| Callback (JSInvokable) | Fires when |
|---|---|
| OnGestureStart(string kind) | Press or hover begins; kind is "press" or "hover". |
| OnGestureEnd(bool success) | Gesture ends; success is false when cancelled (blur, or the pointer left the target). |
Interaction semantics
The runtime press mirrors a real activation, not just a pointer-down. It engages on the primary button of the primary pointer only, so right-clicks and secondary touches are ignored. Keyboard Enter presses and releases the element in parity with a pointer, and a blur while the key is held cancels the press. Hover ignores pointerType === "touch", so a tap never leaves the element hovered.
| Key | Description |
|---|---|
| Enter | Presses the element on keydown and releases it on keyup, in parity with a pointer press. A blur while held cancels. |
Under prefers-reduced-motion (or ReduceMotion = "always") the transform animations are skipped, but the start and end callbacks still fire: behaviour is not decoration. Set ReduceMotion = "never" to opt a single gesture out.