This is the second part of this tutorial. Check out the first part by JcToon, where he covers how we designed and animated Teddy with Rive's State Machine.
A login/signup experience is a great place to bring engaging interactivity into your web apps today. Authentication isn’t normally a fun task for users, so why not make it more playful! Read on to see how you can get your own login forms to stand out among others.
Main Content Overview
Building a Form
CodeSandbox starter template: https://codesandbox.io/s/teddy-login-react-starter-0hrzs0?file=/src/index.js
Building the login animation design: https://rive.app/blog/animated-login-screen-design
Teddy community post: https://rive.app/community/2244-4437-animated-login-screen/
This blog will look at how Rive can make a login experience a more delightful and interactive one that stands out from traditional login/signup UI’s. We’ll be using the rive-react runtime along with the Teddy animation that JC built to build a simple login screen in React. Check out the demo page to get a preview of what we'll be making!
If you prefer to see the code or Rive file, check the top of the blog to find the links. You can also follow along with the CodeSandbox template above to get a starter React app with a form built-in, so you can focus on just integrating the Rive piece in the code. If you follow the CodeSandbox template, skip to the Integrating Rive section below.
To get started building this login experience in your React app, let’s break down some first steps:
Install the Rive dependency
Include the Rive asset into your bundle
In your React project, run npm install rive-react to install the Rive runtime for React, which gets you access to the APIs to integrate Rive files into your app. If you use Typescript, you can also install typescript and its associated packages as devDependencies; we’ll be referencing types throughout this tutorial. Next, download the Rive file from the Teddy community post here, so you have a .riv file. Place that file in a folder in your project that serves up static assets (i.e public/, assets/, etc.) so that you can include them in your app bundle.
We’ll create a new file to encompass all our React logic for this example, so create LoginFormComponent.tsx. Create a component named after the file name and export it as default.
In the next section, we’ll build the form elements and state.
Building a Form
💡Note: We won’t go over building the CSS for this tutorial. Feel free to reference the source code in the link above to see the CSS files
In this application, we’ll be building controlled components that store form state locally in the component. Start by creating state variables that hold the values for our username and password user inputs. While we’re at it, for the simplicity of this tutorial, let’s create a constant for the password that we’ll use to see if the user password is successful for login.
Add the UI to render the form and set the onChange callbacks to store user input. To round out the form, add an onSubmit callback to the <form> element to simulate checking the login credentials. We’ll make this a little more interesting with Rive soon, but for now, keep it empty.
Try and run your app locally by rendering this LoginFormComponent. If you’re using something like create-react-app, try importing the component in the App.js file. You don’t need to pass any props to make this component function.
Great! Our basic form is working. Let’s introduce the Teddy Rive asset we built in the previous blog to make this experience more interesting for users.
💡The rive-react dependency is a wrapper around the web runtime library (i.e @rive-app/canvas). Most exports from the web runtime can be imported from the rive-react dependency too.
Displaying the Animation
The Rive React runtime library has useful hooks and enums to make Rive integration smoother. Let’s start with the useRive hook.
A few things are going on here:
We provide the useRive hook many parameters to instantiate a Rive instance (which we rename riveInstance for clarity), and a <RiveComponent />, which is a React component we can render out in the JSX. We’ll use the Rive instance shortly.
Some of these parameters we supply are essential to getting Rive animations to display:
src - Path to the Rive asset (.riv file), or a URL to a hosted Rive asset
stateMachines - Name of the state machine Rive will play at instantiation
autoplay - Boolean to autoplay the animations
The layout parameter helps to determine how the animation displays within the underlying web canvas
Under the hood, rive-react will do the work of parsing the .riv file to know which animations to display, which artboard to use (in this case, there’s only one, so we don’t need to specify it as a parameter), setting up the canvas and renderer, and more, so you don’t have to worry about these setup details.
If we render out that <RiveComponent /> in the JSX below and add some wrapping <div> elements, you should ultimately see the idle state of Teddy’s default state machine playing on top of the form.
💡 Occasionally, you will have to reference names of components in the Rive file (i.e., artboard name, animation names, state machine names, state machine inputs, etc.). You can find these in the Rive editor when opening the Rive file, or you can use the community-built vscode-rive-viewer extension to see a display of Rive file components. One other method is to console-log riveInstance.contents to see these names as well. Coordinate with design in the building of the Rive asset to come up with consistent naming strategies early on!
Setting up State Machine References
At this point, we’ve got a state machine kicked off when the Rive asset loads and is in the “idle” state. To advance the state machine, we need to manipulate specific inputs associated with the machine to advance the animation states. Ideally, it would be best to collaborate with design counterparts to form the state machine up-front, so both design and dev are aligned. Check out the Teddy Rive file “Login Machine” state machine to understand what inputs drive transitions and the logic behind those transitions.
The rive-react library has a hook we can import called useStateMachineInput which returns a StateMachineInput object given the Rive instance from the useRive hook, the state machine name, and the input name (see the note above at the end of the last section to find different ways to grab references to these names quickly). We use this object to get access to input values, set input values, trigger inputs, and more.
As seen during the build of the state machine in the Rive editor, we have five different inputs that drive various animation states:
isChecking - Boolean input to see if Teddy’s eyes should follow the username input value
isHandsUp - Boolean input to see if Teddy should cover their eyes during user typing for the password input
numLook - Number input to track where Teddy should look from left-to-right on a scale of 0-100, with 0 being all the way left, and 100 being all the way right
trigSuccess - Trigger input to fire when authentication credentials are validated
trigFail - Trigger input to fire when authentication credentials fail
Let's start with integrating one of the more straightforward inputs, isHandsUpInput which dictates whether or not Teddy throws their hands up when the focus is set on the password input. Here, we use the onFocus and onBlur callbacks of the <input> tag to toggle the value for the input. When we toggle the input, the animation should automatically advance the state machine for the next animation state - no extra work needed!
Tracking Username Input
One of the more exciting features of our example login is the ability for Teddy to track user input as the user types in the username field. In the state machine, there's a numLook input that drives where Teddy gazes from left to right on a scale of 0-100.
Because font widths can vary for user input, it can be tricky to create a perfect system for Teddy to track user input, but we can estimate here to get Teddy to look in the general right area! One method to get Teddy's eyes to track close to the user input is to take the width of the input box in pixels and divide that by 100 (the range of values the state machine cares about) to get a multiplier. We can take the product of the multiplier and the number of characters in the text box to get a rough value number we can set on numLook for Teddy to use in the blend state.
Let's start by getting a ref to the input to get the width and create a state variable to track the multiplier we want to calculate. Once we get the width, we can set the multiplier according to the above calculation:
We can utilize this multiplier to set numLookInput.value by taking the product of that multiplier and the number of characters in the username input. By setting the value of numLookInput, we are directly manipulating the state machine, so there is no need to advance any animations or coordinate transitions manually. To round things out, we can set the isCheckingInput value similar to the password input through the onFocus and onBlur callbacks.
Form Submission and Credentials Validation
During form submission, when the login button is pressed, we need to trigger the trigSuccess or trigFail state machine inputs when the credentials are accepted or denied respectively. In a traditional real-world scenario, apps call APIs to validate login credentials. Here, we'll simulate the validation by a simple timeout and form button text change from "Login" to "Checking..." and back to "Login" after a set timeout.
After integrating each of the features above, you should have a functioning simulated login form that tracks multiple user interactions against the state machine we defined earlier. Programatically controlling Rive animations and state machines in simple ways can create a more engaging user experience, especially with tough funnel stages where it's hard for users to get past for one reason or another, such as signup/login.
If you use Rive in your own everyday app experiences like this one, share it with us on Twitter @rive_app!
Join our newsletter
Get all the latest Rive news delivered to your inbox.