Skill 18 · Webflow Mcp:interactions
Subchapter 18.15
references/trigger-mouse-move.mdMarkdown10 KBView on GitHub
| controlType | continuous (omit and the host stamps it) |
| Standalone | Yes — must be the only trigger |
| Target | [REQUIRED] in practice — validation accepts none, the runtime binds nothing. wf:viewport with value: '', or class/selector/attribute/inst |
| Roles | [REQUIRED] on every timeline, unique |
| Playback | [OMIT] all of control, delay, jump, speed |
Mouse-move is absent from TRIGGER_REQUIRES_TARGET_KEYS, so a targetless payload
passes every guard and saves cleanly. It then never fires.
bindTrigger only resolves elements when a target is present:
const targetSchema = trigger[2];
let elements: HTMLElement[] = [];
if (targetSchema) {
elements = this.resolveTargets(targetSchema, {}, interaction);
}ContinuousTriggerStrategy.bind iterates that list, so an empty one means the
mouse-move handler is never invoked. There is no fallback to the viewport, body, or
document: wf:viewport binds to window only because the handler checks for that
extension key explicitly.
The same dead outcome applies when a target is present but resolves to nothing, for
example wf:class with an empty value.
Send {extensionKey: 'wf:viewport', value: ''} unless you specifically want to
bind to elements. Nothing will tell you otherwise: the write succeeds and the
interaction is silently inert.
[REJECTED] smoothness outside its millisecond range, or restingState x/y
outside 0 to 100. Absent keys are legal.
Guard: findMouseMoveRangeError
The panel’s Smoothness slider is 0 to 100 while the persisted bound follows the
number input, so those two ranges differ. Current values are in
capabilities.generated.md → Numeric
bounds.
Exactly 'mouseX', 'mouseY', or 'interval'.
| Role | canvasDuration | wf:mouse-follow action |
|---|---|---|
mouseX / mouseY | allowed | allowed, at most one per timeline |
interval | not allowed | not allowed |
{
pageId,
name: 'Mouse',
triggers: [{
extensionKey: 'wf:mouse-move',
config: {}, // optional pluginConfig: {restingState: {x: 0, y: 0}}
target: {extensionKey: 'wf:viewport', value: ''},
}],
timelines: [
{triggerMetadata: {role: 'mouseX'}, actions: [ACTION]},
{triggerMetadata: {role: 'mouseY'}, actions: [ACTION2]},
],
}Prefer this over wf:transform x/y when the user wants an element to track the
cursor. The transform form works and is what SKILL.md Example 5 shows, but it
hard-codes a travel range; wf:mouse-follow handles anchoring, leave behaviour,
and axis pairing for you.
One action per axis, on the matching mouseX / mouseY timeline. Never on an
interval timeline.
timelines: [
{
triggerMetadata: {role: 'mouseX'},
actions: [{
id: 'act-follow-x',
name: 'Follow X',
timing: {duration: 0.4},
properties: {'wf:mouse-follow': {
axis: 'x',
followMode: 'full',
leaveBehavior: 'stay',
anchor: '0% 0%', // cursor at the target's top-left; omit to centre
groupId: 'mf-cursor',
syncedActionId: 'act-follow-y',
}},
targets: [{extensionKey: 'wf:class', value: [STYLE_BLOCK_ID]}],
}],
},
// …mirror for role 'mouseY' with axis 'y' and syncedActionId 'act-follow-x'
]Values are bare scalars, not [from, to] pairs — there is no from-state to
express. timing is still required, as on every action.
groupId here is not timeline group routing. Two different fields share the
name: timeline.groupId routes a trigger via config.assignedGroupId
(see timelines-and-groups.md), while
wf:mouse-follow‘s groupId pairs the X and Y actions of one follow effect.
Shapes and defaults, from MouseFollowActionConfig:
| Property | Shape | Default | Notes |
|---|---|---|---|
axis | 'x' | 'y' | derived | Optional and not load-bearing — the runtime takes the authoritative axis from the timeline role (mouseX → x, mouseY → y). The store writes it for display only. |
followMode | 'full' | 'x-only' | 'y-only' | 'full' | 'full' keeps X and Y siblings in sync. Absence is treated as 'full', so pre-field data behaves unchanged. |
leaveBehavior | 'return' | 'stay' | 'return' | 'return' tweens back to the start position when the cursor leaves; 'stay' settles at the last position. |
onEnter | 'animate' | 'snap' | — | Behaviour on first enter and every re-enter. Reduced-motion users always snap regardless. |
anchor | "X% Y%" string, or an ix3-origin object | "50% 50%" | Anchor point of the target relative to the cursor. Omitted centres the target on the cursor. |
groupId | string | — | Pairs the X and Y actions of one effect. |
syncedActionId | string | — | The sibling action this one is mirrored with. |
followMode is mirrored between paired actions so both sides agree; setting
'x-only' or 'y-only' on a pair tells the editor’s exit reconciler to delete the
redundant sibling.
The trigger’s own pluginConfig also carries a resting state for when the pointer
is idle or has left: per-axis percentages 0–100, defaulting to {x: 50, y: 50}
(centre).
distance and axes are authored only on an interval timeline.
Shapes:
| Field | Shape | Bounds |
|---|---|---|
distance | integer | finite, 1–10000 |
axes | {x?: boolean, y?: boolean} | per-axis enable bits |
axes is a shared shape for continuous triggers rather than a mouse-move
invention — any plugin driving 2D channels uses the same object instead of
re-implementing it in pluginConfig. Both fields live at
timeline.triggerMetadata, beside role.
triggerMetadata: {role: 'interval', distance: 100, axes: {x: true, y: true}}[REJECTED] Either field on a timeline whose role is not interval. The interval
editor is the only place the Designer writes them.
Guard: findOffIntervalMetadataError · fragment:
only authored on interval mouse-move timelines
[LEGACY-OK-ON-UPDATE] An unchanged echo of a stored value on the same timeline id
passes, so a duration or target read-modify-write is not forced to strip it.
[REJECTED] Interval metadata on an interaction with no wf:mouse-move trigger.
Guard: findIntervalMetadataTriggerError · fragment:
not write them without a "wf:mouse-move" trigger
[LEGACY-OK-ON-UPDATE] Already-stranded stored data passes: if the stored triggers
also lacked mouse-move and the same timeline already carried an interval role with
distance or axes, the update is not introducing the mismatch and is allowed. The
panel gates its interval editor on the role alone, so a user can still edit those
fields on a stranded pair.
Two writes still reject: removing the mouse-move trigger from an interaction that has interval metadata, and newly attaching those fields to an interaction that has no mouse-move trigger.
[REJECTED] A distance that is fractional, or outside 1 to 10000. Bounded in
triggerMetadata‘s schema as z.number().finite().int().min(1).max(10000), so
this fails schema validation before any guard runs. The bound exists to keep a
malicious payload from persisting Infinity or NaN into storage; the runtime
caps fires per update regardless.
[REJECTED] A missing, duplicate, or invalid role on any timeline.
Guard: findTimelineRoleError · fragment: Expected one of: mouseX, mouseY, interval
[REJECTED] Combined with any other trigger.
Guard: findStandaloneTriggerError
[REJECTED] Any of control, delay, jump, speed.
Guard: findUneditablePlaybackFieldError
[REJECTED] wf:viewport as a target on any trigger other than mouse-move.
[REJECTED] A wf:mouse-follow action outside a mouseX/mouseY timeline, or more
than one per timeline. Guard: findMouseFollowContextError
[REJECTED] conditionalLogic — conditions are unavailable and continuous
triggers are additionally excluded.
Guards: findConditionsCapabilityError, findContinuousConditionsError
[REJECTED] conditionalPlayback with behavior: 'skip-to-end' while a
continuous trigger is present. Use dont-animate.
Guard: findConditionalPlaybackError
[PANEL-TRAP] timing.repeat and timing.yoyo on a continuous interaction. The
panel hides both controls when a continuous trigger is present, and
findScrollScrubActionTimingError keys off scrub rather than continuous, so nothing
rejects them.
They are not inert. buildTweensForAction forwards a finite repeat and any
yoyo straight into the GSAP tween vars, and repeat extends the timeline duration
that continuous scrubbing maps gesture progress across. So a value set here changes
how the interaction plays while remaining invisible and uneditable in the panel,
which is the worst combination in this class.
One special case: on a percent canvas an infinite repeat: -1 is materialized to a
single cycle (0), because an infinite duration would break the 0 to 1 scrub
mapping. Outside a percent canvas, -1 passes through unchanged.