How to handle invalid user inputs in React forms for UX design best practices
Tags: react, webdev, uxdesign, webform
Published: 2021-03-15 05:14:41

## TL;DR

_Showing error on blur and hiding it immediately after correction_ is the best practice in web form design. To implement it with React, write code as in [this CodeSandbox demo](https://codesandbox.io/s/number-input-form-uh551?file=/src/NumberInputForm.js). Then we can achieve the user experience like this:![A user enters an invalid text, blurs by clicking next field, sees the error message which disappears immediately after the user corrects the error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dyqe6yk5hj73pzy0vr8a.gif)

## Introduction

### Best UX design practices for web forms

[Wroblewski (2009)], [Holst (2016)], and [Krause (2019)] all say that we should display an error on blur (i.e. when the user leaves a field), rather than immediately after the user has entered an invalid character. [Holst (2016)] reports why, based on their e-commerce checkout usability research:

> "Most people don’t like being told they are wrong – especially when they aren’t. Users therefore naturally find it very frustrating and (quite understandably) feel **unfairly reprimanded** when a site claims they’ve made a mistake before they’ve had a chance to enter a valid input.
>
> "Moreover, during testing, the sudden appearance of an error message disrupted many of the subjects in their typing of a perfectly valid input, as they stopped to read and interpret the error. Even worse, some subjects **misinterpreted this message** to mean that what they had entered so far contained some error and thus began looking for mistakes that weren’t there."

In addition, [Holst (2016)] argues that the error should disappear as soon as the user corrects it, for the following reason:

> "During testing, the subjects ... would look intensely at the error message as they made corrections (i.e. on a keystroke level), expecting the error to be removed as soon as the field contained the valid input. Not removing error messages live, as the error is corrected, can thus cause grave issues as users are likely to misinterpret their newly corrected (and now valid) input to still contain errors."

---

How would you as a web developer implement this best practice in UX design for web forms? For vanilla JavaScript, [Ferdinandi (2017a)] explains how. What about React, without using libraries like Formik?

This article proposes a React implementation of the "Show the error on blur and hide it as soon as the user corrects it" user experience, based on my own experiences of building a form from scratch for my own front-end apps [Line-height Picker](https://line-height-picker.app) and [Triangulum Color Picker](https://triangulum.netlify.app).

### Number Input Field as an Example

As an example of web forms, we will build a number input field for which we probably do not want to use `<input type="number">` for several reasons including:

- There's no way to tell the user why they cannot enter non-numerical characters ([Lanman (2018)])
- Magic Mouse may unintentionally change the input value ([Frost (2019)])
- Removing the tiny arrow buttons is difficult with CSS ([tao (2017)])
- It doesn't work as intended with some screen readers ([Laakso (2020)])

So every web developer should know how to build a number input field from scratch.

However, most of the content below equally applies to other types of text fields like the one for passwords, URLs, e-mail addresses, and so forth.

## Step 1 of 8: Text Input React Way

We start with the standard way of making a text field with React:

```jsx
import { useState } from "react";

export default function NumberInputForm() {
  const [userInput, setUserInput] = useState("");
  const handleChange = (event) => {
    setUserInput(event.target.value);
  };
  return (
    <form>
      <label htmlFor="number-input-field">Enter a number: </label>
      <input
        type="text"
        id="number-input-field"
        onChange={handleChange}
        value={userInput}
      />
    </form>
  );
}
```

For why we should set the `<input>` element's `onChange` and `value` props this way, see [React (2021)].

Even in the case of a single text field, we should wrap the `<label>` and `<input>` elements with the `<form>` element, to allow screen readers to activate the form filling mode ([VanToll (2013)]).

### Optional: Disable the implicit submission

When there is only one `<input>` element within the `<form>` element, we need to disable what's known as _implicit submission_: hitting the Enter key "submits" the input value and resets it by reloading the page (see [VanToll (2013)] for detail). 

We do not want the user to lose the value they have entered if they accidentally hit the Enter key. Some users (like me) may have formed a habit of hitting the Enter key unconsciously once they feel they have entered everything. 

So we add the `submit` event handler to the `<form>` element tag:

```jsx
<form onSubmit={handleSubmit}>
```

and disable its default behavior:

```jsx
  const handleSubmit = event => {
    event.preventDefault();
  };
```

We don't have to do this when there are multiple `<input>` elements inside the `<form>` element. Below we omit this code to make exposition simple.

## Step 2 of 8: Set the keyboard to display for mobile device users

We can disable non-numerical character entry for mobile devices by adding `inputMode="decimal"` to the `<input>` element:

```jsx
      <input
        type="text"
        id="number-input-field"
        inputMode="decimal"      // ADDED
        onChange={handleChange}
        value={userInput}
      />
```

As we're working with React, it's `inputMode`, not the native HTML attribute of `inputmode` (see [React Docs on this](https://reactjs.org/docs/dom-elements.html#all-supported-html-attributes)).

We use `inputMode='decimal'` instead of `inputMode='numeric'` so that not only Android but also iOS show a number pad. See [Holachek (2020)] for more detail.

For other types of text fields (phone numbers, email addresses, URLs, search words), use as the `inputMode` attribute value `"tel"`, `"email"`, `"url"`, `"search"`, respectively. See [Olif (2019)] for more detail.

This way, we can reduce the likelihood of entering invalid inputs to text fields.

## Step 3 of 8: Alert the user on blur

What we want to achieve in Step 3 is to alert the user after they blur the `<input>` element rather than immediately after they enter a non-numeric character. As described at the beginning of this article, that's what UI designers recommend as the best practice.

### Step 3.1: Set the `pattern` attribute value to be a regular expression for expected characters

To alert the user for non-numerical input values, we first need to tell whether the user has entered non-numerical characters. For this purpose, we set the `pattern ` attribute for the `<input>` element:

```jsx
      <input
        type="text"
        id="number-input-field"
        inputMode="decimal"
        onChange={handleChange}
        pattern="[-]?[0-9]*[.,]?[0-9]+"     // ADDED
        value={userInput}
      />
```

The `pattern` attribute takes a regular expression as its value, indicating what characters are accepted. And one way for writing a regular expression for any numbers is as follows ([Ferdinandi (2017b)]):

```
[-]?[0-9]*[.,]?[0-9]+
```

Let me decipher this regular expression step by step. 

First, `[-]?` means the minus sign can be added at the beginning, with `?` indicating either none or one of the preceding character (enclosed in brackets) is allowed. If we don't want the user to enter a negative value, we should remove this.

Next, `[0-9]*` means any integer (no matter how many digits it has) can be added, with `*` indicating zero or any number of the preceding character is allowed. 

So far we've allowed any integer, both positive and negative. If we also want to allow decimals as well, then, first of all, we need to allow a decimal point with `[.,]?` where we allow both Anglo-Saxon (dot) and continental European (comma) ways of writing a decimal point. Then, `[.,]?[0-9]+` means the decimal point should be followed by at least one numerical character, where `+` indicates at least one preceding character is required.

Notice that we allow zero occurrence of numeric characters before the decimal point with `[0-9]*` because some people enter a decimal smaller than 1 in the form of, say, `.39`. 

Note also that the expression `[0-9]+` at the end also means at least one numeric character is required when there is no decimal point, that is, any integer.

Understanding regular expressions is critical for web developers to flexibly set the requirements of user inputs. I recommend [RegexOne](https://regexone.com/), an interactive tutorial thanks to which I've managed to overcome my difficulty of understanding regular expressions.

#### Step 3.2: Add a blur event handler to turn the error on for invalid values

Then we add a blur event handler:

```jsx
export default function NumberInputForm() {
  ...
  // ADDED FROM HERE
  const handleBlur = (event) => {
    if (event.target.validity.patternMismatch) {
    }
  };
  // ADDED UNTIL HERE  
  ...  
  return (
    ...
    <input
      type="text"
      id="number-input-field"
      inputMode="decimal"
      onBlur={handleBlur}              // ADDED
      onChange={handleChange}
      pattern="[-]?[0-9]*[.,]?[0-9]+"
      value={userInput}
      />
  );
}

```

where `event.target.validity.patternMismatch` indicates whether the user has entered a value that does not satisfy the `pattern` attribute value. We create the `error` state and turn it on within its code block:

```jsx
export default function NumberInputForm() {
  ...
  const [error, setError] = useState(false);    // ADDED

  const handleBlur = (event) => {
    if (event.target.validity.patternMismatch) {
      setError(true);                            // ADDED
    }
  };
  ...
}
```

#### Step 3.3: Style the error state

There are several ways to style with CSS in React. For the sake of simple exposition, we use inline styling. (I personally prefer using [`styled-components`](https://styled-components.com/), though.)

```jsx
export default function NumberInputForm() {
  ...
  // ADDED FROM HERE
  function style(error) {
    if (error) {
      return {
        backgroundColor: "rgba(255, 0, 0, 0.5)" 
        // Or any other style you prefer
      };
    }
  }
  // ADDED UNTIL HERE
  
  return (
    ...
      <input
        type="text"
        id="number-input-field"
        inputMode="decimal"
        onBlur={handleBlur}
        onChange={handleChange}
        pattern="[-]?[0-9]*[.,]?[0-9]+"
        style={style(error)}               // ADDED
        value={userInput}
      />
    ...
  );
}

```

#### Step 3.4: Show the error message

The best UI design practice is to tell the user how to correct an invalid value in the text field. To show an error message upon error, we code as follows:

```jsx
export default function NumberInputForm() {
  ...
  return (
    <form>
      <label htmlFor="number-input-field">Enter a number: </label>
      <input
        type="text"
        id="number-input-field"
        inputMode="decimal"
        onBlur={handleBlur}
        onChange={handleChange}
        pattern="[-]?[0-9]*[.,]?[0-9]+"
        style={style(error)}
        value={userInput}
      />
      {/* ADDED FROM HERE */}
      {error && (
        <p role="alert" style={{ color: "rgb(255, 0, 0)" }}>
          Please make sure you've entered a <em>number</em>
        </p>
      )}
      {/* ADDED UNTIL HERE */}
    </form>
  );
}

```

We use the short circuit evaluation (`&&`) so that the error message is injected only when `error` is `true`. (See [Morelli (2017)] for a good introduction to the short circuit evaluation.)

If the `error `variable is `true`, we render a `<p>` element with the `role="alert"` attritube value for accessibility. When an element with this attribute value is programatically inserted into the HTML document, screen readers will read it out (see [MDN Contributors (2021)]).

And we add the inline styling of `style={{ color: "rgb(255, 0, 0)"}}`. This color should be the same hue as the one used to indicate the error state so the user can immediately tell that it's related to the reddened text field. That's a common graphic design technique.

## Step 4 of 8: Forcibly focus the invalid input element on blur

It's best to let the user immediately correct an invalid value in the text field, rather than asking them to click the text field to start correction. 

To do so, we need the `useRef` hook of React. Let me also show the entire code we've built up so far:

```jsx
import { useRef, useState } from "react"; // REVISED

export default function NumberInputForm() {
  const [userInput, setUserInput] = useState("");
  const [error, setError] = useState(false);

  function style(error) {
    if (error) {
      return { backgroundColor: "rgba(255, 0, 0, 0.5)" };
    }
  }

  const ref = useRef();    // ADDED

  const handleBlur = (event) => {
    if (event.target.validity.patternMismatch) {
      ref.current.focus(); // ADDED
      setError(true);
    }
  };

  const handleChange = (event) => {
    setUserInput(event.target.value);
  };

  return (
    <form>
      <label htmlFor="number-input-field">Enter a number: </label>
      <input
        type="text"
        id="number-input-field"
        inputMode="decimal"
        onBlur={handleBlur}
        onChange={handleChange}
        pattern="[-]?[0-9]*[.,]?[0-9]+"
        ref={ref}                           // ADDED
        style={style(error)}
        value={userInput}
      />
      {error && (
        <p role="alert" style={{ color: "rgb(255, 0, 0)" }}>
          Please make sure you've entered a <em>number</em>
        </p>
      )}
    </form>
  );
}

```

 

Programatically focusing a particular element is one example where we should use the `useRef` hook of React. See [React (2020)].

## Step 5 of 8: Remove alert as soon as the user corrects the invalid value

As discussed at the beginning of this article, when the user corrects an invalid value, we should tell them immediately that they have done the right thing, rather than telling them when they blur the `<input>` element. 

To do so, we edit the `handleChange` function:

```jsx
  const handleChange = (event) => {
    // ADDED FROM HERE
    const newValueIsValid = !event.target.validity.patternMismatch;
    if (error) {
      if (newValueIsValid) {
        setError(false);
      }
    }
    // ADDED UNTIL HERE
    setUserInput(event.target.value);
  };
```

The `newValueIsValid` indicates whether a new value the user has just entered is valid or not. If the _previous_ value that the user has entered is invalid (i.e. the `error` state is `true`), then we turn of the error as long as the new value is valid. To avoid re-rendering the UI unnecessarily, we want to update the `error` state only when the `error` is `true`.

---

The remaining three steps below are based on my own preference. But I believe these will contribute to great user experiences on the web form.

## Step 6 of 8: Allow the user to blur the text field once they know there is an error 

With the code so far, there is one problem: when there is an error, the user cannot blur the `<input>` element due to the following bit of code:

```javascript
const handleBlur = (event) => {
    if (event.target.validity.patternMismatch) {
      ref.current.focus();
      setError(true);
    }
  };
```

But maybe the user wants to do something else on the same webpage, before correcting the invalid value. For the first time they blur, we force their cursor to stay in the text field so they can immediately start correcting the invalid value. For the second time they blur, however, we should allow their cursor to exit from the text field.

To do so, we modify the `handleBlur` function as follows:

```javascript
  const handleBlur = (event) => {
    if (!error) {   // ADDED
      if (event.target.validity.patternMismatch) {
        ref.current.focus();
        setError(true);
      }
    } // ADDED
  };
```

We run the code for focusing the `<input>` element only when the `error` is off. When the `error` turns on after the first blurring, then this code block won't run with the second time the user blurs.

## Step 7 of 8: Hide the error message once the user blurs for the second time

However, as the `error ` state persists, the user will see the error message after blurring for the second time. This can be annoying if the error message hides other parts of UI where the user wants to interact with. We want to hide the error message in this case. 

To do so, we need to manage whether or not to show the error message separately from the error state:

```javascript
const [error, setError] = useState(false);
const [showErrorText, setShowErrorText] = useState(false); // ADDED
```

Then, before adding new code for hiding the error message, refactor the rest of the code to achieve the same results so far. For the `handleBlur` function to turn on the error:

```jsx
const handleBlur = (event) => {
    if (!error) {
      if (event.target.validity.patternMismatch) {
        ref.current.focus();
        setError(true);
        setShowErrorText(true);  // ADDED
      }
    }
  };
```

For the `handleChange` function to turn off the error:

```javascript
const handleChange = (event) => {
    const newValueIsValid = !event.target.validity.patternMismatch;
    if (error) {
      if (newValueIsValid) {
        setError(false);
        setShowErrorText(false);  // ADDED
      }
    }
    setUserInput(event.target.value);
  };
```

And for the error message to be added to the DOM:

```jsx
      {showErrorText && (            // REVISED
        <p role="alert" style={{ color: "rgb(255, 0, 0)" }}>
          Please make sure you've entered a <em>number</em>
        </p>
      )}


```

Now it's time to hide the error message after blurring for the second time:

```javascript
const handleBlur = (event) => {
    if (!error) {
      if (event.target.validity.patternMismatch) {
        ref.current.focus();
        setError(true);
        setShowErrorText(true);  
      }
    }
    if (error) {               // ADDED
      setShowErrorText(false); // ADDED
    }                          // ADDED
  };
```

When the user blurs for the second time, the `error` state is already `true`. So only in that case, turn the `showErrorText` state off to hide the error message.

## Step 8 of 8: Show the error message again when the user is going to correct the invalid value

When the user finally wants to correct the invalid value, we should show the error message again to remind them of what values need to be entered. To do so, we add the `focus` event handler:

```javascript
  const handleFocus = () => {
    if (error) {
      setShowErrorText(true);
    }
  };
```

The `handleFocus` function turns the `showErrorText` state on as long as the `error` state is on. 

Then assign this event handler to the `<input>` element:

```jsx
     <input
        type="text"
        id="number-input-field"
        inputMode="decimal"
        onBlur={handleBlur}
        onChange={handleChange}
        onFocus={handleFocus}           // ADDED
        pattern="[-]?[0-9]*[.,]?[0-9]+"
        ref={ref}
        style={style(error)}
        value={userInput}
      />
```

We use the `focus` event handler, rather than a `click` event handler, because the user may use the tab key to focus the `<input>` element. We should show the error message in this case as well.

## Summary

Through the above eight steps, we have built the following component:

```jsx
import { useRef, useState } from "react";

export default function NumberInputForm() {
  const [userInput, setUserInput] = useState("");
  const [error, setError] = useState(false);
  const [showErrorText, setShowErrorText] = useState(false); // ADDED

  function style(error) {
    if (error) {
      return { backgroundColor: "rgba(255, 0, 0, 0.5)" };
    }
  }

  const ref = useRef();

  const handleBlur = (event) => {
    if (!error) {
      if (event.target.validity.patternMismatch) {
        ref.current.focus();
        setError(true);
        setShowErrorText(true);
      }
    }
    if (error) {
      setShowErrorText(false);
    }
  };

  const handleChange = (event) => {
    const newValueIsValid = !event.target.validity.patternMismatch;
    if (error) {
      if (newValueIsValid) {
        setError(false);
        setShowErrorText(false);
      }
    }
    setUserInput(event.target.value);
  };

  const handleFocus = () => {
    if (error) {
      setShowErrorText(true);
    }
  };

  return (
    <form>
      <label htmlFor="number-input-field">Enter a number: </label>
      <input
        type="text"
        id="number-input-field"
        inputMode="decimal"
        onBlur={handleBlur}
        onChange={handleChange}
        onFocus={handleFocus}
        pattern="[-]?[0-9]*[.,]?[0-9]+"
        ref={ref}
        style={style(error)}
        value={userInput}
      />
      {showErrorText && (
        <p role="alert" style={{ color: "rgb(255, 0, 0)" }}>
          Please make sure you've entered a <em>number</em>
        </p>
      )}
    </form>
  );
}

```

This component provides the following user experiences with the number input field:

1. When the user enters a non-numerical character, nothing happens immediately after.
2. But when the user blurs the input field, three things happen: (1) the `<input`> element's background turns into semi-transparent red ( `rgba(255, 0, 0, 0.5)` ), (2) an error message "Please make sure you've entered a _number_" shows up in red (and the screen reader will read it out), (3) the `<input>` element gets focused so the user can immediately start correcting the invalid value.
3. If the user clicks/taps somewhere else to interact with other parts of the webpage before correcting the invalid value, the error message disappears while the semi-transparent red background stays for the `<input>` element.
4. When the user clicks/taps the `<input>` element to start correcting the invalid value, then the error message reappears.
5. As soon as the user finishes correcting the invalid value, the error message disappears and the input field gets back to the default style so the user can quickly tell whether they have entered a valid value or not.

If you need an example of applying the above component into a non-numerical text field, see [the source code of a Hex color code field](https://github.com/masakudamatsu/triangulum-color-picker/blob/main/src/components/TextFieldForHex.js) in my front-end app Triangulum Color Picker.

---

Hope this article will help reduce the number of web forms that irritate users from this world. :-)

## References

Ferdinandi, Chris (2017a) "[Form Validation Part 2: The Constraint Validation API (JavaScript)][Ferdinandi (2017a)]", _CSS-Tricks_, Jun. 27, 2017.

[Ferdinandi (2017a)]: https://css-tricks.com/form-validation-part-2-constraint-validation-api-javascript/

Ferdinandi, Chris (2017b) "[Form Validation Part 1: Constraint Validation in HTML](https://css-tricks.com/form-validation-part-1-constraint-validation-html/)", _CSS-Tricks_, June 26, 2017.

[Ferdinandi (2017b)]: https://css-tricks.com/form-validation-part-1-constraint-validation-html/#validating-numbers

Frost, Brad (2019) "[You probably don’t need input type=“number”][Frost (2019)]", _bradfrost.com_, Mar. 18, 2019.

[Frost (2019)]: https://bradfrost.com/blog/post/you-probably-dont-need-input-typenumber/

Holacheck, (2020) "[Better Form Inputs for Better Mobile User Experiences](https://css-tricks.com/better-form-inputs-for-better-mobile-user-experiences/)", _CSS-Tricks_, Apr. 17, 2020.

[Holachek (2020)]: https://css-tricks.com/better-form-inputs-for-better-mobile-user-experiences/#why-should-i-care-about-inputmode

Holst, Christian (2016) "[Usability Testing of Inline Form Validation: 40% Don’t Have It, 20% Get It Wrong][Holst (2016)]", _Baymard Institute_, Sep. 27, 2016.

[Holst (2016)]: https://baymard.com/blog/inline-form-validation

Krause, Rachel (2019) "[How to Report Errors in Forms: 10 Design Guidelines][Krause (2019)]", _Nielsen Norman Group_, Feb. 3, 2019.

[Krause (2019)]: https://www.nngroup.com/articles/errors-forms-design-guidelines/

Laakso, Hanna (2020) "[Why the GOV.UK Design System team changed the input type for numbers][Laakso (2020)]", _Gov.uk_, Feb. 24, 2020. 

[Laakso (2020)]: https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/

Lanman, Joe (2018) "[Reconsider the behaviour for type="number" - restricting input][Lanman (2018)]", _GitHub Issues_, Apr. 11, 2018

[Lanman (2018)]: https://github.com/w3c/html/issues/1355

MDN Contributors (2021) "[Using the alert role](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_alert_role)", _MDN Web Docs_, Feb. 24, 2021. 

[MDN Contributors (2021)]: https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/ARIA_Techniques/Using_the_alert_role#examples

Morelli, Brandon (2017) "[JavaScript — Short Circuit Conditionals][Morelli (2017)]", _codeburst.io_, Nov 27, 2017.

[Morelli (2017)]: https://codeburst.io/javascript-short-circuit-conditionals-bbc13ac3e9eb

Olif, Christian (2019) "[Everything You Ever Wanted to Know About inputmode][Olif (2019)]", _CSS-Tricks_, May 17, 2019.

[Olif (2019)]: https://css-tricks.com/everything-you-ever-wanted-to-know-about-inputmode/

React (2020) "[Refs and the DOM][React (2020)]", _React Docs_, Sep 21, 2020. 

[React (2020)]: https://reactjs.org/docs/refs-and-the-dom.html

React (2021) "[Forms](https://reactjs.org/docs/forms.html)", _React Docs_, Jan 13, 2021. 

[React (2021)]: https://reactjs.org/docs/forms.html#controlled-components

tao (2017) "[An answer to 'Customizing Increment Arrows on Input of Type Number Using CSS'][tao (2017)]", _Stack Overflow_, Jul. 30, 2017.

[tao (2017)]: https://stackoverflow.com/a/45396364/11847654

VanToll, TJ (2013) "[The Enter Key should Submit Forms, Stop Suppressing it][VanToll (2013)]", _tjvantoll.com_, Jan 1, 2013.

[VanToll (2013)]: https://www.tjvantoll.com/2013/01/01/enter-should-submit-forms-stop-messing-with-that/

Wroblewski, Luke (2009) "[Inline Validation in Web Forms][Wroblewski (2009)]", _A List Apart_, Sep 1, 2009.

[Wroblewski (2009)]: https://alistapart.com/article/inline-validation-in-web-forms/

## Changelog

Sep 24, 2022 (v1.0.2): Add a couple of paragraphs to Step 2 of 8 for clarification.
Sep 16, 2021 (v1.0.1): Add the missing Markdown markup for HTML elements like `<form>`.