Skip to content

wix/interact

Repository files navigation

Wix Interact

Web-native animation and interaction libraries — declarative, AI-ready, framework-agnostic.

npm version npm version npm version license bundle size

What is Interact?

Wix Interact (@wix/interact) is a declarative interaction layer on top of @wix/motion. You describe when something should animate and what should happen in a JSON config — no manual event listeners, no imperative animation wiring.

  • Config-driven — bind triggers (viewEnter, click, hover, viewProgress, pointerMove, and more) to effects in one InteractConfig object
  • Built on native browser APIs — Web Animations API, ViewTimeline, pointer tracking, and CSS; with an optional custom animation runtime via @wix/motion
  • Three entry points — Web Components (@wix/interact/web), React (@wix/interact/react), and vanilla JS (@wix/interact)
  • Ready-made presets — entrance, scroll, pointer, loop, and micro-interactions from @wix/motion-presets
  • SSR-friendly CSSgenerate(config) emits complete CSS for the whole config (keyframes, view-timeline, transitions, FOUC rules) so animations can be ready before JS runs

Live site: wix.github.io/interact · Examples gallery: wix.github.io/interact/examples.html

Packages

Package Description Links
@wix/interact Declarative interaction layer (main package) README · npm
@wix/motion Low-level animation engine README · npm
@wix/motion-presets Ready-made animation presets README npm
@wix/motion ← @wix/interact (declarative layer)
@wix/motion ← @wix/motion-presets (ready-made effects)

Quick Start

Install the interaction layer and presets (presets are required when using namedEffect):

npm install @wix/interact @wix/motion-presets

All examples below share this config — a viewEnter entrance using the FadeIn preset:

import type { InteractConfig } from '@wix/interact';

const config: InteractConfig = {
  interactions: [
    {
      key: 'hero',
      trigger: 'viewEnter',
      params: { threshold: 0.2 },
      effects: [{ effectId: 'hero-fade' }],
    },
  ],
  effects: {
    'hero-fade': {
      duration: 800,
      easing: 'ease-out',
      fill: 'both',
      namedEffect: { type: 'FadeIn' },
    },
  },
};

Web Components (recommended)

Pre-render CSS with generate() to avoid a flash of unstyled content on entrance animations:

import { generate } from '@wix/interact';

const css = generate(config, true); // `true` = use :first-child as default selectors

// then inject into <head>

In HTML template add:

<head>
  <style>
    ${css}
    /* Optional — keep the custom element from affecting layout */
    interact-element {
      display: contents;
    }
  </style>
</head>

Then boot the runtime:

import { Interact } from '@wix/interact/web';
import * as presets from '@wix/motion-presets';

Interact.registerEffects(presets);

Interact.create(config);
<interact-element data-interact-key="hero">
  <section class="hero">
    <h1>Hello, Interact</h1>
  </section>
</interact-element>

React

Wrap Interact.create() in useEffect and destroy on cleanup. Use <Interaction> instead of raw elements:

import { useEffect } from 'react';
import { Interact, Interaction } from '@wix/interact/react';
import * as presets from '@wix/motion-presets';

function App() {
  useEffect(() => {
    Interact.registerEffects(presets);
    const instance = Interact.create(config);

    return () => {
      instance.destroy();
    };
  }, []);

  return (
    <Interaction tagName="section" interactKey="hero" className="hero">
      <h1>Hello, Interact</h1>
    </Interaction>
  );
}

Inject generate(config, false) output into your document's <head> (e.g. Remix links, Next.js layout <head>) the same way as the Web Components example.

Vanilla JS

import { Interact, add } from '@wix/interact';
import * as presets from '@wix/motion-presets';

Interact.registerEffects(presets);
Interact.create(config);

const hero = document.querySelector('.hero') as HTMLElement;
add(hero, 'hero');
<section data-interact-key="hero" class="hero">
  <h1>Hello, Interact</h1>
</section>

Call add(element, key) after the element exists in the DOM. Use remove(key) to unregister a key.

Common Patterns

Config-only recipes — each is a valid InteractConfig shape. Register presets before Interact.create() when using namedEffect.

Entrance animation

const config: InteractConfig = {
  interactions: [
    {
      key: 'card',
      trigger: 'viewEnter',
      params: { threshold: 0.15 },
      effects: [{ effectId: 'card-float' }],
    },
  ],
  effects: {
    'card-float': {
      duration: 900,
      easing: 'cubic-bezier(0.22, 1, 0.36, 1)',
      namedEffect: { type: 'FloatIn', direction: 'bottom' },
    },
  },
};

Inject the styles returned from generate(config) into <head> for FOUC prevention.

Click effect

const config: InteractConfig = {
  interactions: [
    {
      key: 'button',
      trigger: 'activate',
      effects: [
        {
          triggerType: 'repeat',
          duration: 400,
          easing: 'ease-out',
          keyframeEffect: {
            name: 'button-pop',
            keyframes: [
              { transform: 'scale(1)' },
              { transform: 'scale(0.92)' },
              { transform: 'scale(1)' },
            ],
          },
        },
      ],
    },
  ],
};

Use trigger: 'activate' instead of click for keyboard-accessible activation (Enter / Space).

Scroll-driven parallax

const config: InteractConfig = {
  interactions: [
    {
      key: 'parallax-bg',
      trigger: 'viewProgress',
      effects: [
        {
          namedEffect: { type: 'ParallaxScroll', speed: 0.5 },
          rangeStart: { name: 'cover', offset: { unit: 'percentage', value: 0 } },
          rangeEnd: { name: 'cover', offset: { unit: 'percentage', value: 100 } },
          easing: 'linear',
          fill: 'both',
        },
      ],
    },
  ],
};

Replace overflow: hidden with overflow: clip on ancestors between the element and the scroll container — hidden breaks ViewTimeline.

Hover toggle (CSS transition)

const config: InteractConfig = {
  interactions: [
    {
      key: 'card',
      trigger: 'interest',
      effects: [
        {
          key: 'card-figure',
          stateAction: 'toggle',
          transition: {
            duration: 200,
            easing: 'ease-out',
            styleProperties: [
              { name: 'transform', value: 'translateY(-10px)' },
              { name: 'boxShadow', value: '0 12px 24px rgb(0 0 0 / 0.12)' },
            ],
          },
        },
      ],
    },
  ],
};

Use trigger: 'interest' for accessible hover (mouse + keyboard focus).

Pointer-driven custom effect

const config: InteractConfig = {
  interactions: [
    {
      key: 'card',
      trigger: 'pointerMove',
      params: { hitArea: 'root' },
      effects: [
        {
          key: 'spotlight',
          customEffect: (element: HTMLElement, progress: { x: number; y: number }) => {
            const x = progress.x * 100;
            const y = progress.y * 100;
            element.style.background = `radial-gradient(circle at ${x}% ${y}%, rgb(255 255 255 / 0.15), transparent 50%)`;
          },
        },
      ],
    },
  ],
};

Configuration Schema

type InteractConfig = {
  interactions: Interaction[]; // REQUIRED
  effects?: Record<string, Effect>;
  sequences?: Record<string, SequenceConfig>;
  conditions?: Record<string, Condition>;
};

type Interaction = {
  key: string;
  listContainer?: string;
  listItemSelector?: string;
  trigger:
    | 'hover'
    | 'click'
    | 'interest'
    | 'activate'
    | 'viewEnter'
    | 'viewProgress'
    | 'pointerMove'
    | 'animationEnd';
  params?: TriggerParams;
  conditions?: string[];
  selector?: string;
  effects?: (Effect | EffectRef)[];
  sequences?: (SequenceConfig | SequenceConfigRef)[];
};
  • Each Interaction needs at least one of effects or sequences.
  • Each Effect needs exactly one of namedEffect | keyframeEffect | customEffect | transition | transitionProperties.
  • Full spec: full-lean.md

AI and Agent Support

Rules files

@wix/interact:

@wix/motion-presets:

AI generation guidelines

  • Always call Interact.registerEffects(presets) before Interact.create() when using namedEffect
  • Do not invent namedEffect types — use only registered presets (see preset rules above)
  • Do not attach DOM event listeners manually — express behavior through trigger and config
  • For viewProgress, avoid overflow: hidden on ancestors; use overflow: clip instead
  • Call generate(config) at build time or on the server and inject CSS into <head>. For viewEnter + triggerType: 'once', to prevent FOUC
  • effects at the config top level is a reusable Record<string, Effect>
  • <interact-element> should wrap exactly one child (the library targets .firstElementChild by default).

Repository agent context for dev

For monorepo layout, dependency graph, and CLI conventions, see AGENTS.md and CLAUDE.md.

Live Demo and Documentation

Development

Prerequisites: Node.js ≥ 18. Use the repo’s Node version:

nvm use
yarn install
yarn build
yarn test

Local apps:

yarn dev:website    # landing + examples (http://localhost:3000)
yarn dev:docs       # documentation app
yarn dev:demo       # test demo app
yarn workspace @wix/interact-playground run dev   # interactive playground

See CONTRIBUTING.md for contribution workflow and standards.

License

MIT

About

A powerful, declarative interaction library for creating engaging web apps.

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors