How to Add a Rive Login Animation to Your React App
Add a Rive login animation to a React app: install the runtime, wire state machine inputs to the form fields, and fire success and fail triggers.
You know those login forms where a little character watches you type your email, then covers its eyes when you switch to the password field? The ones that make you smile even though you've seen them before?
That's not a video. It's not a GIF. It's a Rive animation with a state machine, and it's way easier to implement in React than you'd think. This post walks you through it, from installing the package to wiring up the animation to your form inputs.
By the end, you'll have a login form where the animation reacts to what the user is doing in real time. Not just playing and looping. Actually responding.
What you need before starting
A React project (Create React App, Vite, Next.js, whatever you use). A .riv file with a login animation that has a state machine. If you don't have one, you can grab one from the Rive Community or from Hoverine. For this tutorial, we'll assume the animation has a state machine called "Login Machine" with these inputs:
isChecking (boolean): the character looks at the email field
isHandsUp (boolean): the character covers its eyes for the password field
numLook (number): controls where the character's eyes track horizontally as the user types
trigSuccess (trigger): fires the success animation
trigFail (trigger): fires the error/shake animation
These are common inputs for login-style Rive animations. Your specific file might name them differently, so check the state machine in the Rive editor before you start.
Step 1: Install the Rive React runtime
npm install @rive-app/react-canvasThat's it. This package wraps the Rive canvas renderer and gives you a React component plus hooks. It's the recommended package unless you specifically need WebGL2, in which case you'd use @rive-app/react-webgl2 instead.
Step 2: Set up the basic component
Create a new component for your login form. We'll use the useRive and useStateMachineInput hooks to load the animation and connect to its state machine inputs.
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
const STATE_MACHINE = 'Login Machine';
export default function LoginForm() {
const { rive, RiveComponent } = useRive({
src: '/login-animation.riv',
stateMachines: STATE_MACHINE,
autoplay: true,
});
const isChecking = useStateMachineInput(rive, STATE_MACHINE, 'isChecking');
const isHandsUp = useStateMachineInput(rive, STATE_MACHINE, 'isHandsUp');
const numLook = useStateMachineInput(rive, STATE_MACHINE, 'numLook');
const trigSuccess = useStateMachineInput(rive, STATE_MACHINE, 'trigSuccess');
const trigFail = useStateMachineInput(rive, STATE_MACHINE, 'trigFail');
return (
<div className="login-container">
<div className="animation-wrapper">
<RiveComponent style={{ width: 400, height: 400 }} />
</div>
<form className="login-form">
{/* We'll add the inputs next */}
</form>
</div>
);
}A couple of things to notice. The useRive hook returns both the rive instance and a RiveComponent that renders the canvas. The state machine inputs are retrieved by name using useStateMachineInput. Each one corresponds to an input you defined (or that the designer defined) in the Rive editor.
Put your .riv file in the /public directory so the src path resolves correctly. If you're using Vite, this works out of the box. For Next.js, same thing.
Step 3: Wire up the email field
The email field is where the character "watches" the user type. We need two things: set isChecking to true when the field is focused, and update numLook as the user types so the character's eyes follow the cursor.
const handleEmailFocus = () => {
if (isHandsUp) isHandsUp.value = false;
if (isChecking) isChecking.value = true;
};
const handleEmailChange = (e) => {
if (numLook) {
// Map the text length to a range the animation expects.
// Most login animations use 0-100, but check yours.
const length = e.target.value.length;
numLook.value = Math.min(length * 3, 100);
}
};Then in your JSX:
<input
type="email"
placeholder="Email"
onFocus={handleEmailFocus}
onChange={handleEmailChange}
/>The numLook mapping is the part you'll probably tweak. The idea is that as the email string gets longer, the character's gaze moves from left to right. Multiplying by 3 is a rough starting point. If the character looks too far too fast, lower the multiplier. If it barely moves, raise it.
Step 4: Wire up the password field
This is the fun part. When the user clicks into the password field, the character covers its eyes. Simple boolean toggle.
const handlePasswordFocus = () => {
if (isChecking) isChecking.value = false;
if (isHandsUp) isHandsUp.value = true;
};
const handlePasswordBlur = () => {
if (isHandsUp) isHandsUp.value = false;
};<input
type="password"
placeholder="Password"
onFocus={handlePasswordFocus}
onBlur={handlePasswordBlur}
/>That's it. Focus the password field, hands go up. Click away, hands come down. The state machine handles the transition animation between states, so you don't need to manage timing or keyframes in code.
Step 5: Handle submit with success and fail animations
Now connect the form submission to fire either the success or fail trigger depending on the result.
const handleSubmit = async (e) => {
e.preventDefault();
// Reset the watching state
if (isChecking) isChecking.value = false;
if (isHandsUp) isHandsUp.value = false;
try {
const response = await loginUser(email, password);
if (response.ok && trigSuccess) {
trigSuccess.fire();
} else if (trigFail) {
trigFail.fire();
}
} catch (err) {
if (trigFail) trigFail.fire();
}
};Trigger inputs are different from booleans. You don't set a value, you .fire() them. They pulse once and the state machine handles the rest. The animation plays the success celebration or the error shake, then returns to idle on its own.
Step 6: Put it all together
Here's the complete component:
import { useState } from 'react';
import { useRive, useStateMachineInput } from '@rive-app/react-canvas';
const STATE_MACHINE = 'Login Machine';
export default function LoginForm() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const { rive, RiveComponent } = useRive({
src: '/login-animation.riv',
stateMachines: STATE_MACHINE,
autoplay: true,
});
const isChecking = useStateMachineInput(rive, STATE_MACHINE, 'isChecking');
const isHandsUp = useStateMachineInput(rive, STATE_MACHINE, 'isHandsUp');
const numLook = useStateMachineInput(rive, STATE_MACHINE, 'numLook');
const trigSuccess = useStateMachineInput(rive, STATE_MACHINE, 'trigSuccess');
const trigFail = useStateMachineInput(rive, STATE_MACHINE, 'trigFail');
const handleEmailFocus = () => {
if (isHandsUp) isHandsUp.value = false;
if (isChecking) isChecking.value = true;
};
const handleEmailChange = (e) => {
setEmail(e.target.value);
if (numLook) {
numLook.value = Math.min(e.target.value.length * 3, 100);
}
};
const handlePasswordFocus = () => {
if (isChecking) isChecking.value = false;
if (isHandsUp) isHandsUp.value = true;
};
const handlePasswordBlur = () => {
if (isHandsUp) isHandsUp.value = false;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (isChecking) isChecking.value = false;
if (isHandsUp) isHandsUp.value = false;
try {
// Replace with your actual auth logic
const success = email === 'user@example.com' && password === 'password';
if (success && trigSuccess) {
trigSuccess.fire();
} else if (trigFail) {
trigFail.fire();
}
} catch (err) {
if (trigFail) trigFail.fire();
}
};
return (
<div style={{ maxWidth: 400, margin: '0 auto', textAlign: 'center' }}>
<RiveComponent style={{ width: 400, height: 300 }} />
<div>
<input
type="email"
placeholder="Email"
value={email}
onFocus={handleEmailFocus}
onChange={handleEmailChange}
style={{ width: '100%', padding: 12, marginBottom: 12 }}
/>
<input
type="password"
placeholder="Password"
value={password}
onFocus={handlePasswordFocus}
onBlur={handlePasswordBlur}
onChange={(e) => setPassword(e.target.value)}
style={{ width: '100%', padding: 12, marginBottom: 12 }}
/>
<button onClick={handleSubmit} style={{ width: '100%', padding: 12 }}>
Sign In
</button>
</div>
</div>
);
}If you’d rather skip the wiring, Login Glasses is a premium template that does this whole routine. It’s a pair of round glasses with heavy eyebrows. The pupils follow the pointer, the brows lift on the email field, and the eyes squeeze shut when you reach the password. Show the password and one eye opens to peek. A wrong password drops the brows and shakes the glasses, and a good login gets a little hop. It works differently from the code above, though. The email and password fields are real Rive text inputs inside the file, and validation runs on data binding instead of state machine inputs. You set Email Valid and Password Correct from your app, or turn Auto Check off and fire Success and Fail yourself. That means the input names in Step 2 won’t apply here. Open the file in Riv Eye first and see what it exposes.
Things to watch out for
Canvas sizing. The RiveComponent needs a container with defined dimensions. If it renders at 0x0, the animation won't show. Always set explicit width and height on the component or its wrapper.
Conditional rendering. If you're showing/hiding the login form based on state, wrap the Rive component in its own sub-component. The Rive docs strongly recommend this because the canvas context ties to a specific DOM element, and React's unmount/remount cycle can break it.
Input names must match exactly. The strings you pass to useStateMachineInput need to match what's in the .riv file. A typo means the input silently returns null and nothing happens. If things aren't working, open the file in the Rive editor and double-check the state machine and input names.
Null checks matter. Every state machine input can be null (the animation might not have loaded yet, or the name might not match). Always check before setting values. The if (isHandsUp) isHandsUp.value = true pattern handles this cleanly.
Where to go from here
This pattern works for any Rive animation with a state machine, not just login forms. The same useRive + useStateMachineInput approach applies to interactive buttons, onboarding flows, dashboards, game UIs, whatever. The animation handles the visual logic. Your React code just tells it what state to be in.
If you're looking for login animations (or any other Rive template) to drop into your project, check out Hoverine. Every template comes as an editable .riv file with documented state machine inputs, so you can follow this exact pattern and be up and running in minutes instead of hours.
Now go make your login form the part of your app people actually enjoy using.