Free tool

Next.js Rive State Machine Wrapper Generator

Generate a copy-paste Next.js and Rive component with dynamic import, reduced-motion fallback, lazy-mount, and a typed state-machine handle. Works with App Router and React 19.

InstantNo sign-upUpdated May 31, 2026

State machine inputs

'use client';

/**
 * YokaifyMascot — Next.js + Rive wrapper (Yokaify generator)
 *
 * Lazy-loads the Rive runtime, wires a state machine, and exposes a typed
 * imperative handle for parent components to drive inputs.
 *
 * Bundle cost: 0 KB on the initial chunk (Rive runtime is dynamic-imported).
 * Respects prefers-reduced-motion: static-poster.
 */

import {
  forwardRef,
  useEffect,
  useImperativeHandle,
  useRef,
  useState,
} from 'react';
import dynamic from 'next/dynamic';
import type { StateMachineInput } from '@rive-app/react-canvas';

const Rive = dynamic(() => import('@rive-app/react-canvas').then((m) => m.default ?? m.useRive), {
  ssr: false,
  loading: () => <Poster width={240} height={240} />,
});

export interface YokaifyMascotHandle {
  setIsAttentive: (v: boolean) => void;
  setMood: (v: number) => void;
  fireWave: () => void;
}

export interface YokaifyMascotProps {
  className?: string;
  /** Skip mounting until the element is in viewport */
  lazy?: boolean;
}

export const YokaifyMascot = forwardRef<YokaifyMascotHandle, YokaifyMascotProps>(
  function YokaifyMascot(props, ref) {
    const containerRef = useRef<HTMLDivElement | null>(null);
    const [mounted, setMounted] = useState(false);
    const [reducedMotion, setReducedMotion] = useState(false);
    const inputs = useRef<StateMachineInput[]>([]);

    // prefers-reduced-motion guard
    useEffect(() => {
      const mq = window.matchMedia('(prefers-reduced-motion: reduce)');
      const update = () => setReducedMotion(mq.matches);
      update();
      mq.addEventListener('change', update);
      return () => mq.removeEventListener('change', update);
    }, []);

    // Lazy-mount via IntersectionObserver
    useEffect(() => {
      if (!containerRef.current || mounted) return;
      const io = new IntersectionObserver(
        (entries) => {
          if (entries.some((e) => e.isIntersecting)) {
            setMounted(true);
            io.disconnect();
          }
        },
        { rootMargin: '200px' }
      );
      io.observe(containerRef.current);
      return () => io.disconnect();
    }, [mounted]);


    // Imperative handle for parents to drive state-machine inputs
    useImperativeHandle(ref, () => ({
      setIsAttentive: (v: boolean) => {
        const i = inputs.current.find((x) => x.name === "isAttentive");
        if (i) i.value = v;
      },
      setMood: (v: number) => {
        const i = inputs.current.find((x) => x.name === "mood");
        if (i) i.value = v;
      },
      fireWave: () => {
        const i = inputs.current.find((x) => x.name === "wave");
        if (i && typeof i.fire === 'function') i.fire();
      },
    }), []);

    return (
      <div
        ref={containerRef}
        className={props.className}
        style={{ width: 240, height: 240, contain: 'layout paint' }}
      >
        {mounted && !reducedMotion ? (
          <Rive
            src="/rive/yokaify-mascot.riv"
            artboard="Mascot"
            stateMachines="State Machine 1"
            autoplay
            fit="contain"
            onLoad={(e: any) => {
              // Capture inputs once the file loads.
              if (e?.detail?.inputs) inputs.current = e.detail.inputs;
            }}
            style= width: '100%', height: '100%' 
          />
        ) : (
          <Poster width={240} height={240} />
        )}
      </div>
    );
  }
);

function Poster({ width, height }: { width: number; height: number }) {
  return (
    <div
      role="img"
      aria-label="Animation placeholder"
      style= width, height, background: 'linear-gradient(135deg,#f5f5f5,#eaeaea)', borderRadius: 12 
    />
  );
}

Requires @rive-app/react-canvas. Drop the component file under src/components/ and the usage example wherever you render the mascot.

What this generates

Rive is a browser-only animation engine, so it cannot run on the server. This tool builds a 'use client' wrapper that loads Rive the right way in Next.js:

  • Dynamic import keeps the runtime out of your initial bundle, so it adds nothing to the first server-rendered HTML.
  • Lazy-mount with IntersectionObserver waits until the mascot is near the viewport, so it does not slow down LCP.
  • Reduced-motion fallback shows a static poster when the visitor prefers reduced motion. No animation, no runtime download.

Drop the output into src/components/. It works with App Router and React 19.

How to use it

  1. Add your state-machine inputs in the tool above.
  2. Copy the Component tab into a file under src/components/.
  3. Copy the Usage example tab to see how to drive it from a parent.
  4. Put your .riv file in public/rive/.

Driving the state machine

Rive inputs come in three types: boolean, number, and trigger. The generator gives you a typed handle with one method each: set for boolean and number inputs, fire for triggers.

const mascotRef = useRef<YokaifyMascotHandle>(null);

// Drive boolean inputs
mascotRef.current?.setIsAttentive(true);

// Drive number inputs
mascotRef.current?.setMood(0.7);

// Fire trigger inputs
mascotRef.current?.fireWave;

Inputs are called imperatively, so changing them never re-renders the wrapper.

When to turn lazy-mount on

  • Hero mascot, above the fold: turn it off so the mascot loads right away.
  • Below the fold: leave it on; it loads as the visitor scrolls near it.
  • Inside a modal or drawer: leave it on; it waits until the element is needed.
Copy the Component tab into your project and drive the mascot from a parent.

Reduced motion

When prefers-reduced-motion is on, the wrapper shows a static poster instead of mounting Rive, and the runtime never downloads. The poster is a simple gradient div you can swap for your own still frame.

File structure for the generator output

src/
└── components/
    ├── YokaifyMascot.tsx        # The wrapper (Component tab)
    └── MascotConsumer.tsx       # The usage example (Usage example tab)
public/
└── rive/
    └── yokaify-mascot.riv

The wrapper references it as /rive/yokaify-mascot.riv — Next.js serves anything in public/ at the root path, so no import or fetch path adjustment is needed.

Further reading

Frequently asked questions

Client Component, always. Rive needs the browser canvas API and a paint loop. Server Component wrapper that re-exports a Client Component child adds nothing technically.