Login Form Mascot Animation: How Teddy-Style Logins Work
By the Charmotion team · Updated

A login form mascot is a Rive file whose state machine your form code drives. In Rive's teddy example, a boolean makes the bear look at the username field, a number steers its gaze, another boolean covers its eyes during password entry, and success or fail triggers play after submit. Wire focus, typing and the login result to those inputs.
Key takeaways
- Rive's teddy uses isChecking, numLook, isHandsUp, trigSuccess and trigFail.
- Map focus to attention, typing to thinking, waiting to loading, and the result to success, no or error.
- Debounce typing and never let the animation delay the login itself.
- Never animate from the password's content or length.
- Covering the eyes and following the caret need custom animations built in the Rive editor.
How does Rive's teddy login animation work?
Rive's "Animated login screen design" post (March 2022) builds the well-known teddy bear login and lists its state machine inputs:
isChecking(boolean): true while the username field is active, which moves the bear from looking idle to looking at the field.numLook(number): drives a blend between looking left and right; "when the input value is 0, the look is on the left; when the input value is 100 the look is on the right."isHandsUp(boolean): "Teddy covers his eyes with his hands when the user enters the password," and lowers them when it's false.trigSuccessandtrigFail(triggers): play a reaction after login, then return to idle.
The post doesn't show the app code. How numLook is calculated is up to you; a common choice is the caret position in the username field, scaled to 0 to 100.
The idea generalizes: the form owns the events, the Rive file owns the motion, and a handful of named inputs connect them. Rive also has pointer listeners that can make an object follow the cursor inside the canvas, set up in the editor.
Which form events should trigger which states?
| Form event | Mascot state | How |
|---|---|---|
| Page load | enter, then idle | Trigger, then mood 0 |
| Email or username focused | attention (or listening) | Mood 9 (or 6) |
| Typing | thinking | Mood 3, back to attention after a pause |
| Password focused | a calm state, or a custom "cover eyes" animation | Mood 0, or your own boolean |
| Submitting | loading | Mood 5 |
| Signed in | success, then happy | Trigger, then mood 1 |
| Wrong password | no | Trigger, stay on the form |
| Network or server error | error, then sad | Trigger, then mood 4 |
The mood numbers are the convention used in Charmotion files (0 idle, 1 happy, 3 thinking, 4 sad, 5 loading, 6 listening, 9 attention); a hand-built file will have its own names. Keep "wrong password" gentle: a head shake, not a dramatic failure, because people mistype constantly.
How do I wire form events to Rive in React?
With Data Binding, load the file with autoBind: true and use the view model hooks for the mood number and each trigger. This component is self-contained; replace signIn with your auth call.
import { useRef } from "react";
import {
useRive,
useViewModelInstanceNumber,
useViewModelInstanceTrigger,
} from "@rive-app/react-canvas";
const MOOD = { idle: 0, happy: 1, thinking: 3, sad: 4, loading: 5, attention: 9 };
export function LoginForm({ signIn }: { signIn: (e: string, p: string) => Promise<"ok" | "wrong" | "error"> }) {
const { rive, RiveComponent } = useRive({
src: "/mascot.riv",
stateMachine: "CharacterViewModelMachine",
autoplay: true,
autoBind: true,
});
const vmi = rive?.viewModelInstance;
const { setValue: setMood } = useViewModelInstanceNumber("mood", vmi);
const { trigger: success } = useViewModelInstanceTrigger("success", vmi);
const { trigger: no } = useViewModelInstanceTrigger("no", vmi);
const { trigger: error } = useViewModelInstanceTrigger("error", vmi);
const typing = useRef<number>();
const onType = () => {
setMood?.(MOOD.thinking);
window.clearTimeout(typing.current);
typing.current = window.setTimeout(() => setMood?.(MOOD.attention), 800);
};
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const f = new FormData(e.currentTarget);
setMood?.(MOOD.loading);
const result = await signIn(String(f.get("email")), String(f.get("password")));
if (result === "ok") { success?.(); setMood?.(MOOD.happy); }
else if (result === "wrong") { no?.(); setMood?.(MOOD.idle); }
else { error?.(); setMood?.(MOOD.sad); }
};
return (
<form onSubmit={onSubmit}>
<div style={{ width: 160, height: 160 }} aria-hidden="true"><RiveComponent /></div>
<input name="email" type="email" autoComplete="email"
onFocus={() => setMood?.(MOOD.attention)} onInput={onType}
onBlur={() => setMood?.(MOOD.idle)} />
<input name="password" type="password" autoComplete="current-password"
onFocus={() => setMood?.(MOOD.idle)} />
<button type="submit">Sign in</button>
</form>
);
}
Notes: the setters are undefined until the file loads, hence the ?. calls. The password field only sets a calm state on focus; it never reports what's typed. With an input-based file (like the teddy), use useStateMachineInput to get each input and set .value or call .fire() instead. More patterns are in Rive in React and Next.js.
How do I keep a login mascot from getting in the way?
- The form comes first. Submit immediately; never wait for an animation to finish before calling your auth API or redirecting.
- Debounce typing. Set "thinking" on input and return to a resting state after a short pause, as above. Don't fire a trigger per keystroke.
- Don't leak the password. Eye-tracking by character count is charming on a username field; on a password field it shows onlookers how long the password is.
- Accessibility. Mark the mascot
aria-hidden="true", keep real error text next to the field, and honorprefers-reduced-motionby holding a static pose. - Weight. Rive's lightest web runtime,
canvas-lite2.44.0, measured about 373 KB brotli on October 25, 2026, loaded once per site. Lazy-load it so it doesn't slow the first paint of your sign-in page.
What can't a generated login mascot do?
The two signature moves of the teddy login are custom animations: hands covering the eyes, and eyes following the caret through a blend state. A standard state set doesn't include them.
Charmotion's sets are an example. From one image you get the states in the mapping table (attention, thinking, loading, success, no, error and more) in a .riv wired for Data Binding, with React code, on every paid plan (from $20/month). There is no "cover eyes" state and no eye or cursor tracking. The delivered .riv is a runtime file, not an editable project, so adding them means building those animations in the Rive editor from your own artwork or hiring a Rive animator for custom work. Charmotion is automated and doesn't add animations to a set; ask support@charmotion.app before buying if you need something specific.
Frequently asked questions
Does a login mascot slow down the sign-in page?
It adds a runtime and a file. Rive's canvas-lite 2.44.0 web runtime measured about 373 KB brotli-compressed on October 25, 2026 (once per site), and a character file is often tens of kilobytes. Lazy-load the mascot after the form renders so the form is usable first.
Should the login mascot cover its eyes on the password field?
It's a nice touch if your file has that animation, and it signals privacy. Don't fake it by tracking password length, which reveals information. Without a custom animation, a calm idle state on password focus works fine.
Can a Rive mascot follow the mouse cursor?
Inside its canvas, yes, if the file is built for it. Rive's editor has pointer listeners, and the Align Target action makes an object follow the pointer. That has to be set up in the editor; it isn't something runtime code can add to a file that lacks it.
Sources
- Animated login screen design, Rive Blog
- Listeners, Rive
- Data binding (React runtime), Rive
- Runtime sizes, Rive
- prefers-reduced-motion, MDN Web Docs