Multistep Forms in React with Awesome UX – Animations

30 Aug 2021

4 minute read

Your typical visitor is accustomed to page loads. A fresh request for a new HTML page will clear the viewport of content, trigger a browser’s loading indicators, and usually takes a moment or two to complete. These are unmistakable indicators that “something new” is occurring, and cue us to start looking out for those new somethings.

When a SPA framework like React loads a new “page,” this does not occur. Because the pages are being built dynamically inside the browser itself instead of being requested from a backend server, those hundreds or thousands of milliseconds of page loading may happen instantaneously. The URL may change, the page content may change, but most of the usual “new page” indicators will be absent.

If you’re in a “prime” state of mind – well rested, attentive, engaged with the task at hand – then you’ll probably notice these super snappy transitions, and you’ll be well oriented to what’s going on.

Many people, sadly, are not in this “prime” state of mind most of the time: maybe they haven’t been sleeping well, maybe the kids are banging on pots in the kitchen, maybe thye’ve got a killer hangover, who knows. Whatever the case, when people miss these transitions, they may start to feel confused, alienated, or frustrated. This is a bad user experience!

We don’t have any control over things like native loading indicators, but we have other ways of indicating that the content of a page has changed. Animations can help us signal to our visitors that there is new content on the page, and fortunately, they’re pretty easy to add! I really like Animate.css for basic animations:

Be judicious in your use of animations, however. Check out the best practices guidelines in the docs for a great primer on when to use animations and when to skip them.

Compare and contrast

Let’s take a look at some demo pages, both with and without animations.

No animations example

If you click the “Go to next step” button, the content will change. I’ve added in some goofy emojis to make the different parts of the form extra obvious, but the transitions would be easy to miss without them:

Example with animations

This is the exact same page with the fadeInUp animation from Animate.css added in. A very small, simple change, but it makes a huge difference! The transition point between steps is clear and unambigious.


Here’s the full example code, simplified for ease of reading.

This might look a little funny compared to “normal” React, but the concepts are all the same. Instead of using Node modules to include React in the app, we’re using JS module imports from instead.

Instead of using JSX, which requires a transpilation step to transform it into React.createElement calls, we’re using an extremely cool no-transpilation JSX alternative: HTM.

You can dig into more specifics about the code and how it works here.

import { h, Component, render } from "";
import htm from "";
import { useState } from "";

const html = htm.bind(h);

const Page = (props) => {
  const { label, title, onPageChange } = props

  return html`<div class="animate__animated animate__fadeInUp">
      <label for="demo-1">${label}</label>
        placeholder="Fill in a value! (or not)"
    <div class="">
      <button onClick=${() => onPageChange()}>
        Go to next step

const PageOne = (props) => {
  const { onPageChange } = props
  return html`
      title="Step one ☝️" 
      label="What is your name?" 
      onPageChange=${onPageChange} />`

const PageTwo = (props) => {
  const { onPageChange } = props
  return html`
      title="Step two ✌️" 
      label="What is your quest?" 
      onPageChange=${onPageChange} />`

const routes = { 
  "one": PageOne,
  "two": PageTwo

const App = (props) => {  
  const [url, setUrl] = useState("");
  const [page, setPage] = useState("one")

  const onPageChange = () => {
    if (page === "one") {

    if (page === "two") {

  return html`
    <div aria-live="polite">
      <${routes[page]} onPageChange=${onPageChange} />

render(html`<${App} />`, document.getElementById("app-root"));

Next up

Next week, we’ll take a look at persistent state for multistep forms. With most React apps, your state will only persist for a single browser session. If you’ve got a long form, or if you need to take the user away from the form to log in or validate with something like OAuth, then it’s possible that your visitors won’t be able to finish and submit the whole thing in a single browser session, and will lose all of their data!

How do we save the state of what’s already been entered into a form, either for impatient visitors or for instances where they have to leave the domain of your app temporarily? Thankfully, there are many in-browser strategies we can use, and we’ll learn about them next week.

Until then!

Table of contents:

This is a series of blog posts which will cover each aspect of a great multi-step form experience separately. Check back for a new post each Monday until they’re all done!

Leave a comment

Related Posts