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.
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
- Add your state-machine inputs in the tool above.
- Copy the Component tab into a file under
src/components/. - Copy the Usage example tab to see how to drive it from a parent.
- Put your
.rivfile inpublic/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.
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
- GuideNext.js chatbot 2026 architecture guideWhere this wrapper fits in the broader Next.js implementation.
- GuideRive animation for ecommerceThe animation runtime layer of the chat widget.
- ToolFramer Motion spring slider builderFor non-Rive animations in the same Next.js app.
- BlogRive in Next.js — `use client` patternsThe deep dive on the dynamic-import pattern.
- BlogRive state machines — behavioral logic without re-renderingHow state machines drive the mascot states.
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.