interactor
Install, integrate, and configure Interact — @wix/interact — declarative interactions library — to add or edit hover/click/view triggered, scroll-driven, and…
npx skills add https://github.com/wix/interact --skill interactorInteractor — build interactions with @wix/interact
This skill installs, wires up, and configures motion interactions so you can add or edit interactions on any webpage or web app. It is interact-first: you describe what should animate and when as a declarative JSON config, and the library does the DOM wiring. You almost never call the motion engine directly.
Mental model — packages and one config
| Package | Role | You touch it… |
|---|---|---|
@wix/interact | Declarative layer. Binds triggers → effects via an InteractConfig. Ships vanilla / React / Web-Component entry points. | Always. This is the API. |
@wix/motion-presets | Ready-made named effects (entrance, scroll, ongoing, mouse). Referenced as namedEffect: { type: 'FadeIn' }. | When you want a prebuilt effect (the common case). |
@wix/motion | The engine (WAAPI, CSS, ViewTimeline, fastdom). Bundled inside interact. | Rarely — only for programmatic/escape-hatch animation. See references/motion-engine.md. |
@wix/interact-validate | Static validator for InteractConfig shape (schema + referential checks). No DOM. | Agent-side validation always; optional dev/CI guard in user projects. See references/validate.md. |
The whole job is: pick a trigger, pick an effect, bind it to an element with a key. Everything else is detail.
┌── trigger (when) ──┐ ┌── effect (what) ────────────────┐
│ viewEnter, hover, │ ───► │ namedEffect: { type: 'FadeIn' } │ ──► applied to
│ click, viewProgress│ │ duration, easing, triggerType │ element with
│ pointerMove, … │ │ (or keyframeEffect/customEffect)│ matching key
└────────────────────┘ └─────────────────────────────────┘
Workflow
Follow four steps in order: Install → Integrate → Add/Edit interactions → Validate. If the project already uses interact (a config and the package exist), skip to Add/Edit. Read the linked reference files as you reach each step — they hold the full schema, the preset catalog, and per-trigger rules. Don't try to hold it all in your head; the references are the source of truth.
Step 1 — Install
Both packages, one command. @wix/motion comes transitively inside
@wix/interact — do not install it separately on this path.
npm install @wix/interact @wix/motion-presets
# (yarn add / pnpm add work too — match the project's package manager)
npm install -D @wix/interact-validate # optional — permanent dev/CI config guard only
A no-build / plain-HTML site can skip npm and import from a CDN instead — see the
CDN recipe in references/integration-recipes.md. CDN pages skip the validate
package install; the agent validates configs without shipping the validator.
Step 2 — Integrate
First, detect the stack and pick the entry point (this determines every import and a couple of flags). Decision procedure:
- React / Next / any JSX project (a
package.jsonwithreact,.jsx/.tsxfiles) → use@wix/interact/reactwith the<Interaction>component. - Plain HTML, no bundler (static
.html, no build step) → use@wix/interact/webwith<interact-element>custom elements, imported from a CDN. - Bundled vanilla JS / other framework (Vite/Webpack but no React, or Vue/Svelte/Angular) → use
@wix/interact/web(Web Components are framework-agnostic) or the base@wix/interactvanilla API. Prefer/webunless the user wants to control binding manually.
If you can't tell, ask the user which framework the page uses. The full
copy-paste setup for each entry point — including SSR, cleanup, and a verification
snippet — is in references/integration-recipes.md. Read it now for the entry
point you chose.
The shape is the same everywhere:
import { Interact, generate } from '@wix/interact/web'; // or /react, or '@wix/interact'
import { FadeIn } from '@wix/motion-presets'; // import only what you use (tree-shakes)
Interact.registerEffects({ FadeIn }); // BEFORE generate()/create() — see invariants
const css = generate(config, /* useFirstChild */ true); // inject into <head>
const instance = Interact.create(config); // wire it up
(For CDN/quick-start, import * as presets + registerEffects(presets) is fine —
selective imports just keep bundled apps lean. See references/presets.md.)
Mark up target elements with a key that matches the config:
<!-- web -->
<interact-element data-interact-key="hero"><section>…</section></interact-element>
<!-- react -->
<Interaction tagName="section" interactKey="hero">…</Interaction>
<!-- vanilla -->
<section data-interact-key="hero">…</section>
// for vanilla - add the following
import { add } from '@wix/interact';
const el = document.querySelector('[data-interact-key="hero"]');
add(el);
Step 3 — Add / edit interactions
This is where most work happens. An InteractConfig is:
{
interactions: [ // REQUIRED — each binds one source+trigger to effect(s)
{ key, trigger, params?, effects?, sequences?, conditions?, selector?, listContainer?, listItemSelector? }
],
effects?: { [effectId]: Effect }, // reusable effects, referenced by effectId
sequences?: { [sequenceId]: SequenceConfig },
conditions?:{ [conditionId]: Condition }, // media/selector gates
}
To add an interaction:
- Choose the trigger (see decision table below).
- Choose the effect: prefer a
namedEffectpreset (browsereferences/presets.md); fall back to inlinekeyframeEffectfor custom keyframes, orcustomEffectfor non-CSS (SVG/canvas/text). - Set the playback field the trigger needs:
triggerTypefor time effects on hover/click/viewEnter;stateActionfor CSS-state (transition) effects;rangeStart/rangeEndforviewProgress. Never set bothtriggerTypeandstateActionon one effect. - Bind it: give the target element the matching
keyin the markup.
To edit an existing config: read the current config first, find the
interaction/effect by its key/effectId, and change only what's asked.
Preserve the rest (other interactions, ids, markup keys). After editing, re-run
validation (Step 4) — a changed namedEffect.type or a new
viewProgress effect can silently break if you skip it. If the effect catalog or
trigger semantics are involved, open references/presets.md / references/triggers.md.
For multi-target staggering (cards, lists, nav items), use sequences, not
manual per-item delays — see references/triggers.md and the sequences section of
references/config-schema.md.
Step 4 — Validate the config
No InteractConfig reaches generate() / Interact.create() unvalidated, and
no @wix/interact-validate reference ships in the code you deliver — on any
entry point, CDN included. How you run validation depends on whether you can
construct the config statically:
- Static config (you authored a literal you can read in full): validate before
emit in a scratch script — never add validator imports to user files. See
references/validate.mdfor per-environment run mechanics. - Dynamic config (built at runtime from data/props/fetch/loops — you cannot
construct it by reading): temporarily inject
assertValidInteractConfig(config)immediately beforegenerate()/create(), run so that code path executes, fix everyseverity: 'error', then remove the call, import, anyesm.shimport, and any temp devDep. Prefer a dev-only validation script when the config builder module is importable in isolation (no removal step). Full loop inreferences/validate.md. - Permanent guard (opt-in, separate): leaving
assertValidInteractConfigin shipped code as a devDependency CI gate is only when scaffolding a new project or the user explicitly asks — do not conflate with the temporary injection above.
Fix every issue with severity: 'error' before proceeding; prefer fixing warnings
too. valid: false blocks emit.
Before declaring done, grep the files you're shipping:
grep -REn 'interact-validate|validateInteractConfig|assertValidInteractConfig|InteractValidationError' <shipped files>
# expect: no matches (unless the user asked for a permanent CI guard)
Then run the semantic checklist below.
Trigger → use-case quick reference
| Trigger | Use for | Effect type & key field |
|---|---|---|
viewEnter | Entrance animations when an element scrolls into view | Time effect; triggerType (default 'once') |
viewProgress | Scroll-driven (parallax, reveal, scrub tied to scroll position) | Scrub effect; rangeStart/rangeEnd |
hover / interest | Hover effects (interest = hover+focus, accessible) | Time effect (triggerType) or State effect (stateAction) |
click / activate | Click toggles (activate = click+keyboard, accessible) | Time effect (triggerType) or State effect (stateAction) |
pointerMove | Cursor-following / tilt / parallax-on-mouse | Scrub effect; params.hitArea, params.axis |
animationEnd | Chain one effect after another finishes | params.effectId of the preceding effect |
Per-trigger deep rules and gotchas → references/triggers.md. Effect catalog (which preset for which look) → references/presets.md. Full
field-by-field schema for every config object → references/config-schema.md.
Critical invariants — get these wrong and output silently breaks
These are the failure modes that don't throw — the page just renders wrong or the animation no-ops. Apply them every time, even if you don't open a reference file.
-
registerEffects()runs BEFOREgenerate()andInteract.create(). An unregisterednamedEffect.typedoesn't error — it logs a console warning and the animation never runs. Register the presets you use up front — prefer a selectiveimport { FadeIn, … }(tree-shakeable) overimport * as presetsin bundled apps. -
generate(config, useFirstChild)parity — passtruefor the web (<interact-element>) entry point,falsefor vanilla and React. Backwards = the FOUC-prevention selectors target the wrong node and break. -
FOUC prevention — inject
generate()CSS. Always inject thegenerate()output (ideally at SSR/build) so entrance elements are hidden before JS runs. The generated CSS includes FOUC-prevention rules (gated by:not([data-interact-enter])) forviewEnter+onceentrances where source and target are the same element; when source ≠ target (e.g. stagger viaselector), the generated rules hide the targets on their own. Forrepeat/alternate/state, inline the starting keyframe and usefill: 'both'. -
Vanilla binding. You must then call the standalone
add(element, 'key')for each element once it exists in the DOM. For clean up call theremove('key')function.add/removeare functions importedfrom the package. -
viewEnterwith same source & target → onlytriggerType: 'once'. Forrepeat/alternate/state, the animation can move the element out of/into the viewport and re-trigger forever. Use separate source and target elements for those. -
Hit-area shift. On
hoverorpointerMove, if the effect changes the element's size/position (scale,translate), the hovered hit-area shifts and flickers. Keep the trigger on the stable parent and animate a child by puttingselector(or differentkey) on the effect —selectoron the effect sets the target;selectoron the interaction sets the trigger's source instead (the opposite of what you want). -
viewProgressneedsoverflow: clip, nothidden.overflow: hiddenon any ancestor between the element and the scroll container creates a scroll context that kills ViewTimeline. Replace everyoverflow: hiddenwithoverflow: clip(Tailwind:overflow-clip). -
Never invent or guess. Use only real preset names (
references/presets.md). If you don't know a preset's option name/type, omit it and rely on defaults — guessing produces silently-wrong output. Never emitDVD(exists in types but isn't registered) or anyBg*/ImageParallaxpreset (experimental, not production-ready). For "background parallax", use the publicParallaxScrollon the image element withviewProgress. -
Scroll presets carry a
range. Every*Scrollpreset needsrange: 'in' | 'out' | 'continuous'in itsnamedEffect(prefer'continuous') — exceptParallaxScroll, which takesparallaxFactorinstead. -
Lists: one keyed wrapper, fan out by
selectororlistContainer— never duplicate keys. Keys are unique (one controller per key), so never put the same key on N repeated elements — they'd clobber and only the last binds. Instead key an ancestor wrapper and choose by who triggers: useselectoron the effect when one trigger staggers/animates many targets (aviewEntersequence over cards); uselistContaineron the interaction when each item needs its own trigger (per-cardhover/pointerMove, one tracker each). Either way theselector/listContainermust match a descendant of the keyed element, not the keyed element itself.
Verify your work (run before declaring done)
Animations are hard to confirm headlessly, so this static check is your reliable proxy.
Automated config validation
-
validateInteractConfig(config)returnsvalid: true(noseverity: 'error'issues). Seereferences/validate.md. - Shipped files contain no
interact-validate,validateInteractConfig,assertValidInteractConfig, orInteractValidationErrorreferences (unless the user explicitly asked for a permanent CI guard).
Semantic & integration checklist
Items the validator cannot check — walk these after automated validation passes:
- Every
namedEffect.typeis a real registered preset fromreferences/presets.md(notDVD, not aBg*preset, not invented). - Every
*Scrollpreset used withviewProgresshas arange(exceptParallaxScroll). -
pointerMoveeffects have norangeStart/rangeEnd(those areviewProgress-only). - Every interaction
key(and effectkey) has a matching element in the markup (data-interact-key/interactKey). -
generate()output is injected anduseFirstChildmatches the entry point. - Child-target effects put
selector/keyon the effect, not the interaction. Groups of items use one keyed wrapper + a descendant match (no duplicate keys):selectoron the effect for a one-trigger stagger/sequence,listContaineron the interaction for per-item triggers. - Invariants 5–7 and 10 hold for the relevant triggers (separate source/target, child targets,
overflow: clip, unique keys).
If a dev server is available, load the page and confirm the animation runs and the browser console is free of "not found in registry" warnings.
Reference files
Read the one(s) relevant to the task — they are self-contained and source-accurate:
references/config-schema.md— every config object field-by-field:InteractConfig,Interaction, all three effect variants, sequences, conditions, element resolution (source vs target), FOUC, and the fullInteractstatic API.references/triggers.md— per-trigger deep rules and gotchas:viewEnter,viewProgress,hover/click(+triggerType/stateActiontables),pointerMove,animationEnd, accessibility variants, and sequences/stagger.references/presets.md— the full preset catalog by category with parameters, defaults, accessibility risk tiers + reduced-motion fallbacks, and an "atmosphere → preset" selection guide.references/integration-recipes.md— complete copy-paste setup per entry point (web / React / vanilla / CDN), with SSR, lifecycle/cleanup, and verification.references/validate.md— how to run@wix/interact-validate(static scratch script vs temporary injection for dynamic configs), options, limitations, and what the validator does not check.references/motion-engine.md— thin escape-hatch reference for calling@wix/motiondirectly (programmaticgetWebAnimation/getScrubScene/getSequence), easings, and engine gotchas. Only when the declarative config can't express what's needed.