Skip to content
charmotion

AI Elements Persona vs a Custom Rive Character

By the Charmotion team · Updated

Cover illustration for the AI assistant avatars guide: AI Elements Persona vs a Custom Rive Character, with Charmotion demo characters

AI Elements Persona is a Vercel React component that renders an animated AI visual with Rive WebGL2. It has five states (idle, listening, thinking, speaking, asleep) and six variants, with Obsidian as the default. Its documentation doesn't describe loading your own .riv, so a branded mascot means writing a small Rive component with the same state names.

Key takeaways

  • Persona's states are idle, listening, thinking, speaking and asleep; its variants are Obsidian, Mana, Opal, Halo, Glint and Command.
  • The documented props are state, variant, className and lifecycle callbacks.
  • Custom .riv files are not documented, so treat Persona as a set of abstract visuals, not a character loader.
  • A wrapper that accepts the same state prop lets you swap Persona for a branded character without touching chat logic.

Already have a character or logo? Preview your own character animated in a phone splash, a website hero and an empty state — free, in your browser, nothing uploaded.

What does the AI Elements Persona component do?

AI Elements is Vercel's library of React components for AI interfaces, built to pair with the AI SDK. Persona is its avatar piece. The docs describe it as a component that "displays an animated AI visual that responds to different conversational states" (AI Elements: Persona).

Under the hood it renders with Rive on WebGL2, so state changes blend instead of cutting between clips. It also handles a React detail that trips up hand-rolled WebGL components: the docs mention a built-in guard that defers WebGL2 initialization by one frame, so no context is created during Strict Mode's throw-away development mount.

Like other AI Elements components, Persona is added to your project as source code rather than installed as an opaque package:

npx ai-elements@latest add persona
"use client";

import { Persona } from "@/components/ai-elements/persona";

export function AssistantVisual({ state }: { state: "idle" | "listening" | "thinking" | "speaking" | "asleep" }) {
  return <Persona state={state} className="size-24" />;
}

Because it uses browser APIs, render it from a client component in the Next.js App Router. Omitting variant gives you the default Obsidian look.

Which states and variants does it support?

Persona supports five conversational states and six visual variants, according to its documentation (AI Elements: Persona):

PropValuesNotes
stateidle, listening, thinking, speaking, asleepDrive it from your chat or voice status
variantObsidian (default), Mana, Opal, Halo, Glint, CommandAbstract visual styles
classNameCSS classesSize and placement
Lifecycle callbacksonLoad, onReady, onPlay, onPause, onStop, onLoadErrorLoading states and error fallback

The five states line up with how assistants behave, and they match the conventions in most avatar guides: listen while the user talks, think while the model works, speak while it answers, sleep when the session is inactive.

What is not in the list is just as useful to know. There are no one-shot reactions such as success or error, and no "loading" state separate from thinking. If your product needs to celebrate a completed task or flag a failure visually, you will show that elsewhere in the UI.

Mapping from the AI SDK is short. With useChat, submitted becomes thinking, streaming becomes speaking and everything else idle (useChat reference):

const personaState = status === "submitted" ? "thinking" : status === "streaming" ? "speaking" : "idle";

Can I use my own character in Persona?

Not in any documented way. Persona's docs list the props above and the six built-in variants; they don't describe passing your own .riv file or adding a variant (AI Elements: Persona). As of October 2026, treat Persona as a set of ready-made abstract visuals.

Because the component is copied into your codebase, you can read how it loads its Rive files and what it sets on them. Editing it to point at a different file is possible in principle, but it is undocumented, your file would have to expose exactly the inputs or properties the component drives, and updating the component later would overwrite your changes.

The cleaner route is a tiny component of your own that accepts the same state prop. Your chat logic keeps producing "idle" | "listening" | "thinking" | "speaking" | "asleep", and you choose at render time whether that goes to Persona or to your character. Rive's own React runtime handles the file with a few lines, as the next section shows; see Rive in React and Next.js for setup details.

How do Persona states map to a 10-mood character?

A character with numbered moods covers all five Persona states and adds room for reactions. With the ten-mood layout used elsewhere in these guides, the mapping is direct:

Persona stateMoodValue
idleidle0
listeninglistening6
thinkingthinking3
speakingtalking7
asleepsleeping8
"use client";

import { useEffect } from "react";
import { useRive, useViewModelInstanceNumber } from "@rive-app/react-webgl2";

export type PersonaState = "idle" | "listening" | "thinking" | "speaking" | "asleep";

const MOOD: Record<PersonaState, number> = { idle: 0, listening: 6, thinking: 3, speaking: 7, asleep: 8 };

export function CharacterPersona({ state, className }: { state: PersonaState; className?: string }) {
  const { rive, RiveComponent } = useRive({
    src: "/character.riv",
    stateMachine: "CharacterViewModelMachine",
    autoplay: true,
    autoBind: true,
  });
  const { setValue: setMood } = useViewModelInstanceNumber("mood", rive?.viewModelInstance);

  useEffect(() => {
    setMood(MOOD[state]);
  }, [state, setMood]);

  return (
    <div aria-hidden="true" className={className}>
      <RiveComponent />
    </div>
  );
}

The hooks come from Rive's React data binding API (Rive React data binding). Because the props match, switching between <Persona state={s} /> and <CharacterPersona state={s} /> is a one-line change. You can then add the reactions Persona lacks with useViewModelInstanceTrigger("success", ...).

How do I handle loading and errors with Persona or a custom character?

Both options download a Rive runtime and a .riv file before anything appears, so plan for the first moments and for failure. Persona exposes lifecycle callbacks for this, including onLoad, onReady and onLoadError (AI Elements: Persona). A custom component gets the same information from useRive: the rive instance stays null until the file has loaded.

  • Reserve the space. Give the wrapper a fixed size so the chat layout doesn't jump when the canvas appears.
  • Show a still first. A static image of the idle pose behind the canvas avoids an empty box on slow connections.
  • Fall back quietly. If loading fails, keep the still image and carry on. The assistant must work without its avatar.
  • Keep status in text. Whether the visual is an orb or a mascot, a label such as "Thinking" tells everyone what is happening, including screen reader users.

For pausing offscreen, reduced motion and canvas remounts, the rules are identical for both; see accessible, fast AI avatar animation.

When is a branded mascot better than an abstract orb?

Neither is better in general. They signal different things.

Persona (abstract visual)Custom character
Setup timeMinutesDesign and animation work, or a generated set
Brand recognitionShared look across many appsYours alone
StatesFive, fixedWhatever the file has
Reactions (success, error)Not includedPossible with triggers
Expectation it sets"This is a system""This is our product's personality"

Choose Persona when you are prototyping, when the assistant is a utility inside a larger tool, or when you want the neutral, ambient feel of an orb. Choose a character when the assistant is the product, when you already have a mascot or logo that people recognize, or when you need reactions beyond the five states, such as celebrating a completed booking.

The cost gap has narrowed. Charmotion turns an existing character or logo into a 17-animation set in about one to two hours, with the ten moods above and six triggers in one .riv (Studio plan, $29/month), plus MP4, GIF and transparent WebM/MOV exports for places a canvas can't go. Whatever you choose, keep the state logic separate from the visual, as in the wrapper above, so the decision stays reversible.

Chat assistant screens with an animated avatar listening, thinking, talking and confirming
Assistant avatar states mapped to the conversation.

Frequently asked questions

What states does the AI Elements Persona component support?

Persona supports five states: idle, listening, thinking, speaking and asleep. It renders with Rive on WebGL2 and comes in six variants: Obsidian (the default), Mana, Opal, Halo, Glint and Command. The documented props are state, variant, className and lifecycle callbacks such as onLoad and onLoadError.

Can I load a custom .riv file into Persona?

The Persona documentation doesn't describe loading your own .riv or adding variants. Since the component's source is copied into your project, editing it is technically possible but undocumented. A small Rive component that accepts the same state prop is the more maintainable way to use a branded character.

Does Persona work with React Strict Mode?

Yes. Its docs describe a built-in guard that defers WebGL2 initialization by one frame, so no context is created during Strict Mode's throw-away development mount. If you write your own Rive avatar, keep useRive and RiveComponent in one stable component to avoid remount problems.

Quick answers from this guide

Sources

Related guides