DEV Community

Cover image for Build a State-Aware AI Agent Mascot with Rive and React
Praneeth Kawya Thathsara
Praneeth Kawya Thathsara

Posted on

Build a State-Aware AI Agent Mascot with Rive and React

Build a State-Aware AI Agent Mascot with Rive and React

How to connect real application events to an expressive, accessible AI assistant interface without pretending the model knows more than it does.

Most AI assistant interfaces have the same visual vocabulary: a text field, a response area, and a loading indicator.

That is enough for a basic question-and-answer experience. It becomes less useful when an AI agent can search documents, invoke tools, request approval, recover from errors, and execute a sequence of tasks.

Consider an assistant that is preparing a meeting summary. It may be fetching notes, generating a draft, waiting for permission to email attendees, or handling a failed calendar request.

A single spinning icon does not communicate those differences.

An interactive mascot can make the workflow easier to interpret—but only when its animation is connected to real application state.

In this tutorial, we'll design an event-driven status architecture and show how Rive and React can work together to present an expressive agent interface.

The goal is not to make an AI system appear human. It is to make software behavior more legible.

1. Define what the mascot actually represents

Before opening Rive, define the states your application can reliably identify.

For an AI assistant, a practical starting set is:

State Meaning Visual behavior
Idle No active operation Calm breathing and occasional blinking
Working A confirmed task is executing Focused expression and subtle movement
Waiting The system needs user input Attentive pose and visible prompt
Success A defined operation completed Brief positive acknowledgment
Error An operation failed Neutral concern and recovery cue
Disconnected Status cannot be verified Static or subdued visual with explanation

These states are deliberately operational.

Avoid naming a state understanding, feelingConfident, or knowsTheAnswer. Those labels imply internal knowledge or emotion that your application cannot establish.

A model may produce confident text while a downstream tool fails. Your mascot should represent the tool failure, not the model's apparent confidence.

Separate transient events from persistent states

A task can remain in working for thirty seconds, while a success animation may need to play only once.

That distinction matters.

Persistent states describe the current condition. Transient events describe something that happened.

For example:

  • TASK_STARTED transitions to working.
  • APPROVAL_REQUIRED transitions to waiting.
  • TASK_COMPLETED transitions to success.
  • TASK_FAILED transitions to error.
  • TASK_RESET returns to idle.

The application should own these transitions. The mascot should visualize them.

2. Design the Rive character around the state model

Rive is particularly relevant because its state machines allow interactive graphics to react to application input.

Rive also supports Data Binding, which connects structured data to visual properties. Rive introduced general availability for Data Binding in April 2025 and published further guidance on View Models and state-machine integration in July 2025.

The implementation should begin with a compact character rig rather than six unrelated illustrations.

A reusable rig might contain:

  • Head and torso controls.
  • Eye direction and blink controls.
  • Eyebrow and mouth shapes.
  • Two or three arm poses.
  • A subtle secondary-motion system.
  • A status accent that remains readable without color.

Keep the silhouette consistent. The character should remain recognizable when switching between states.

Establish the motion language

Each state needs a distinct motion signature.

Idle: Low-frequency breathing, occasional blinking, and minimal visual noise.

Working: Small eye movements, focused posture, or a repeating secondary-motion loop.

Waiting: A deliberate attention shift toward the user-facing approval control.

Success: One brief acknowledgment followed by a return to the appropriate stable state.

Error: A restrained reaction that does not turn a serious failure into a joke.

Disconnected: A clear visual reduction in activity, paired with explanatory text.

The transitions should not imply that more motion means more intelligence.

A character that moves continuously can become distracting, particularly when the user is reading detailed results.

3. Choose a state-machine contract

There are two reasonable ways to connect application state to Rive.

The first uses traditional state-machine inputs. The second uses Rive Data Binding and View Models.

For a small proof of concept, state-machine inputs are straightforward.

For a growing product, a View Model can provide a more explicit contract between design and engineering.

An illustrative View Model could be named AgentStatusVM and contain:

Property Type Purpose
status Enum Current operational state
progress Number Verified progress, if available
hasPendingApproval Boolean Whether user authorization is required
statusText String Short user-facing status
isConnected Boolean Whether live status is available

These are proposed properties, not built-in Rive properties.

In the Rive Editor, bind the properties to relevant visual elements and state-machine conditions.

For example, the status enum can select an animation state, while progress can control a visual progress indicator.

Do not animate a percentage unless the backend can provide meaningful progress. A looping activity indicator is more truthful than a fabricated 83% completion value.

4. Build the application-side state reducer

A React interface should receive validated events from the agent orchestration layer.

The following TypeScript example demonstrates an application-side state model. It does not depend on a particular AI provider.

type AgentStatus =
  | "idle"
  | "working"
  | "waiting"
  | "success"
  | "error"
  | "disconnected";

type AgentState = {
  status: AgentStatus;
  message: string;
  taskId: string | null;
};

type AgentEvent =
  | { type: "TASK_STARTED"; taskId: string }
  | { type: "APPROVAL_REQUIRED"; taskId: string }
  | { type: "TASK_COMPLETED"; taskId: string }
  | { type: "TASK_FAILED"; taskId: string }
  | { type: "CONNECTION_LOST" }
  | { type: "TASK_RESET" };

const initialState: AgentState = {
  status: "idle",
  message: "Ready",
  taskId: null,
};

function agentReducer(
  state: AgentState,
  event: AgentEvent
): AgentState {
  if (event.type === "CONNECTION_LOST") {
    return {
      ...state,
      status: "disconnected",
      message: "Connection lost. Status unavailable.",
    };
  }

  if (event.type === "TASK_RESET") {
    return initialState;
  }

  if (event.type === "TASK_STARTED") {
    return {
      status: "working",
      message: "Working on your request",
      taskId: event.taskId,
    };
  }

  if (event.taskId !== state.taskId) {
    return state;
  }

  switch (event.type) {
    case "APPROVAL_REQUIRED":
      return {
        ...state,
        status: "waiting",
        message: "Your approval is required",
      };

    case "TASK_COMPLETED":
      return {
        ...state,
        status: "success",
        message: "Task completed",
      };

    case "TASK_FAILED":
      return {
        ...state,
        status: "error",
        message: "Task failed. Review the details.",
      };
  }
}
Enter fullscreen mode Exit fullscreen mode

The task ID prevents an event from an unrelated task from overwriting the current visual state.

A production reducer needs more safeguards. In particular, it should handle event sequence numbers, retries, cancellation, reconnection, and explicit transition rules.

For example, a delayed TASK_COMPLETED event should not automatically override a newer task's waiting state.

The key architectural principle remains simple:

The orchestration layer reports events. The reducer establishes UI state. The mascot renders that state.

5. Connect the state to a Rive character in React

Rive provides React runtimes for rendering interactive .riv files.

The example below illustrates a traditional state-machine-input integration using @rive-app/react-canvas.

Before using it, create a Rive file containing a state machine named AgentMachine with a numeric input named StatusCode. Configure transitions for values 0 through 5.

The names and mappings are conventions for this example, not Rive defaults.

import { useEffect } from "react";
import {
  useRive,
  useStateMachineInput,
} from "@rive-app/react-canvas";

type AgentStatus =
  | "idle"
  | "working"
  | "waiting"
  | "success"
  | "error"
  | "disconnected";

const statusCodes: Record<AgentStatus, number> = {
  idle: 0,
  working: 1,
  waiting: 2,
  success: 3,
  error: 4,
  disconnected: 5,
};

type Props = {
  status: AgentStatus;
  message: string;
};

export function AgentMascot({ status, message }: Props) {
  const { rive, RiveComponent } = useRive({
    src: "/agent-mascot.riv",
    stateMachines: "AgentMachine",
    autoplay: true,
  });

  const statusInput = useStateMachineInput(
    rive,
    "AgentMachine",
    "StatusCode"
  );

  useEffect(() => {
    if (statusInput) {
      statusInput.value = statusCodes[status];
    }
  }, [statusInput, status]);

  return (
    <section aria-label="AI assistant status">
      <div aria-hidden="true">
        <RiveComponent />
      </div>
      <p role="status" aria-live="polite">
        {message}
      </p>
    </section>
  );
}
Enter fullscreen mode Exit fullscreen mode

This is an illustrative integration pattern, not a tested production application.

The .riv file must exist, the state-machine names must match, and the transitions must be configured in the editor.

For new projects, also evaluate Rive's current Data Binding APIs and the runtime package appropriate to your renderer and platform.

Why keep the message outside the animation?

Because the message is meaningful application information.

A user who cannot see the animation should still know whether the agent is working, waiting, or reporting an error.

Keeping a semantic text representation in the DOM also makes localization and automated UI testing easier.

In a real application, avoid repeatedly announcing unchanged status messages. Use a dedicated alert or appropriately prioritized announcement for failures that require immediate attention.

6. Add Data Binding when the interface becomes richer

Numeric state inputs work for a small example, but they become cumbersome when the character must also reflect task progress, approval requirements, user preferences, and status text.

That is where View Models become valuable.

A View Model can define a structured contract between the React application and the Rive design.

The application can update properties, while the Rive file determines how those values affect the character.

Conceptually, the integration looks like this:

// Conceptual pseudocode — not a Rive runtime API example.

function synchronizeMascot(agentState) {
  mascotViewModel.status = agentState.status;
  mascotViewModel.statusText = agentState.message;
  mascotViewModel.hasPendingApproval =
    agentState.status === "waiting";
}
Enter fullscreen mode Exit fullscreen mode

The actual runtime calls depend on the Rive package and version in use.

Refer to the official runtime documentation before implementing property access, View Model instances, or listeners.

This separation is useful because it allows the animation designer to refine transitions without requiring the application to know how every eyebrow or arm movement works.

The engineering team owns the data contract. The animation team owns the visual behavior.

7. Handle asynchronous failures carefully

AI agents are asynchronous systems.

Tool calls can time out. Network connections can drop. A user can cancel a task while another operation is still running.

Those cases should be designed explicitly.

Avoid false success

Do not play a success animation simply because the language model produced a final message.

For a task such as sending an email, success should depend on the relevant application or provider confirmation.

Avoid endless working loops

A mascot that remains enthusiastically busy for several minutes may conceal a stalled operation.

Consider introducing a longer-running status message, a cancellation option, or a recovery action after a defined interval.

Do not automatically label a slow operation as failed unless the system has evidence of failure.

Treat approval as a real interaction

If the agent needs permission to perform an external action, show a conventional, accessible approval interface.

The mascot can direct attention toward it, but should never replace explicit confirmation.

Handle stale events

Use task identifiers and, where necessary, monotonically increasing event sequence numbers.

After reconnecting, fetch authoritative task status instead of assuming the last locally displayed state remains correct.

8. Make the mascot accessible and optional

Animation should supplement information rather than carry it exclusively.

A useful accessibility checklist includes:

  • Keep all important status messages available as text.
  • Respect reduced-motion preferences.
  • Avoid rapid flashing and unnecessary looping.
  • Ensure approval controls are keyboard accessible.
  • Use readable contrast and do not rely solely on color.
  • Provide a nonanimated fallback.
  • Test screen-reader announcements with real workflows.

For reduced motion, you can render a static state illustration or suppress nonessential character motion.

Do not assume setting a CSS media query on the canvas will stop animations inside the Rive runtime. Control the animation playback or provide a suitable alternative at the component level.

An expressive assistant should not make the application harder to use.

9. Test the system, not just the animation

A visually appealing mascot can still misrepresent application behavior.

Test the full event-to-animation path.

At minimum, verify these scenarios:

  1. A task starts and completes successfully.
  2. A task requests approval.
  3. The user declines approval.
  4. A tool call fails.
  5. A network connection is lost.
  6. A new task starts before an older task finishes.
  7. The interface is used with reduced motion.
  8. The animation fails to load.

Also measure practical implementation concerns:

  • Initial asset loading.
  • Runtime memory usage.
  • Animation frame stability on lower-powered devices.
  • Layout stability.
  • Status announcement frequency.
  • Interaction latency.
  • Error and fallback behavior.

Rive's October 1, 2026 runtime update describes performance improvements across several platforms, but those changes should not be treated as a guarantee for any particular mascot file.

Test your actual rig, artboard, runtime version, and target devices.

10. When should you build a custom mascot?

A custom Rive mascot makes the most sense when visual identity and interactive feedback are recurring parts of the product.

A simple spinner may be sufficient for a short, predictable background request.

A mascot becomes more interesting when the product has:

  • Repeated multi-step agent workflows.
  • User approval and recovery states.
  • A recognizable assistant identity.
  • Frequent voice or conversational interactions.
  • Multiple UI surfaces that share the same character.

The investment is not just in illustration. It is in creating a reusable visual system with rigging, transitions, states, and an application-facing contract.

For teams that need that complete system, Mascot Engine creates custom illustrated, rigged, interactive Rive mascots for AI products and applications.

The scope can include multiple operational states, expressive character behavior, and developer integration guidance, depending on project requirements.

You can also explore animation work at RiveAnimator.com.

An animated AI assistant should not merely look active.

It should communicate what the application actually knows.

The strongest architecture separates backend events, application state, and visual behavior. Rive then becomes a presentation layer that can express those states through a coherent character.

Start with a reliable status contract. Design a small number of meaningful states. Test failures and accessibility before adding more personality.

Top comments (0)