DEV Community

Cover image for Native form validation is easy
Charles F. Munat for Craft Code

Posted on • Edited on • Originally published at craft-code.dev

Native form validation is easy

You can validate your forms without JavaScript.

Less than 6 minutes, 1470 words, 4th grade

It is easy to create web forms that validate themselves without JavaScript. Why so many developers ignore this native capability is an open question.

I guess that when all you have is React or similar, then everything looks like a React component.

But what happens when the user disables JavaScript (or it is otherwise unavailable)? OK, you are rendering your React components on the server. Nice. But what happens to your client-side validation?

But before we get into this, a brief digression. Letʼs take another look at our previous article, Progressive enhancement.

The requestAnimationFrame option

After I published that article, I got a suggestion from a good friend. He recommended that I animate the accordion elements with requestAnimationFrame. So I tried it:

function toggleAccordion (event) {
  event.preventDefault()

  const summary = event.target
  const accordion = summary.closest("details")
  const content =
    accordion.querySelector(".xx-accordion-content")
  const openHeight = summary.xxOpenHeight

  if (accordion.open) {
    function shutAccordion (
      height,
      stopHeight = 0,
      decrement = 20
    ) {
      return function () {
        content.style.maxHeight = `${height}px`

        if (height <= stopHeight) {
          accordion.open = false

          return
        }

        requestAnimationFrame(
          shutAccordion(
            height - decrement,
            stopHeight,
            decrement
          )
        )
      }
    }

    shutAccordion(openHeight)()

    return
  }
  function openAccordion (
    height,
    stopHeight,
    increment = 10
  ) {
    return function () {
      content.style.maxHeight = `${height}px`

      if (height >= stopHeight) {
        return
      }

      requestAnimationFrame(
        openAccordion(
          height + increment,
          stopHeight,
          increment
        )
      )
    }
  }

  accordion.open = true

  openAccordion(0, openHeight)()
}
Enter fullscreen mode Exit fullscreen mode

I wonʼt go into a lot of detail here as it is pretty self-explanatory. We create open and close outer functions that return an inner closure. requestAnimationFrame calls that closure recursively. In this way, we can increment or decrement the height on each frame immutably.

Is this a potential performance bottleneck, creating all those closures? Maybe. But itʼs the simplest way to do it, and we donʼt prematurely optimize here at Craft Code. If it turns out to be a bottleneck, weʼll switch to a loop and a mutable variable.

Betcha it works just fine.

But which is better? setTimeout? Or requestAnimationFrame?

Itʼs a pretty close call. setTimeout is older and will work in browsers such as Internet Explorer pre-v10. Of course we can polyfill that.

An argument for requestAnimationFrame is that it may be smoother. Especially if there are many simultaneous animations on the page. It also may be more performant. Donʼt need to support very old browsers? Then requestAnimationFrame is probably the better way to go.

You should ask, “Where in the world are my users? What browsers are they using?” That will help you to determine which option is best. But then we should always start with that question, right?

Try it on our example animation frame accordion.

About those forms

Letʼs get to our forms. A few simple recommendations to start.

  • Donʼt use placeholders. Just donʼt. Use <label> instead. Set the for attribute to the id of the input labelled.
  • Group related controls in <fieldset> elements. Use the <legend> element to label the group. Fieldsets are easily styled with CSS these days.
  • Ensure that your form elements are keyboard navigable. And in the same order that they appear visually. Donʼt confuse your users.
  • You can best determine other considerations, such as where to put buttons or which buttons to use, with user testing. There is no one right way.

With those caveats in mind, let us begin with a simple form. Here is one with which we are all familiar … but not for much longer, we hope.

<form
  action=""
  class="xx-form"
  method="GET"
  name="simple-form"
>
  <fieldset class="xx-fieldset">
    <legend>Please sign in</legend>

    <div class="xx-form-field">
      <label
        class="xx-field-label"
        for="email"
        id="xx-email-label"
      >
        Email address
      </label>
      <br>
      <div class="xx-field-help" id="xx-email-help">
        The email address with which you signed up.
      </div>
      <input
        aria-labelledby="xx-email-help xx-email-label"
        class="xx-field-input xx-email-field"
        id="email"
        name="email"
        required
        size="36"
        type="email"
      >
    </div>

    <div class="xx-form-field">
      <label
        class="xx-field-label"
        for="password"
        id="xx-password-label"
      >
        Password
      </label>
      <br>
      <div class="xx-field-help" id="xx-password-help">
        Four or more space-separated words of 4+ characters.
      </div>
      <input
        aria-labelledby="xx-password-help xx-password-label"
        class="xx-field-input xx-password-field"
        id="password"
        name="password"
        pattern="[a-zA-Z]{4,}( [a-zA-Z]{4,}){3,}"
        required
        size="36"
        type="password"
      >
    </div>
  </fieldset>

  <button
    class="xx-submit-button"
    type="submit"
  >
    Sign in
  </button>
</form>
Enter fullscreen mode Exit fullscreen mode

As in previous examples, we use CSS class names on all our elements. The xx- is a namespace, i.e., cc- for Craft Code. This allows us to select our elements in our CSS and avoids collisions.

We like grouping controls in fieldsets, and using the legend for the title of our form. We group our labels and inputs in <div> elements with the class name, xx-form-field. This permits us to style them as a group.

Our view is that best practice is to put the label above the input. To make this work even when the user disables CSS, we use a <br> element. Note that we tie our labels to their inputs with the for attribute.

Whenever possible, we choose our HTML elements to take advantage of browser features. We chose an <input> of type email rather than type text. Because of this, the browser will validate the value of the input on submit.

If the value is not a potentially valid email address, then submission fails.

We also set the required attribute on the input. The form will require a valid email address before we can submit it.

One down.

Then for the password field, we choose the password type, which masks the characters as we type them. We also set the required attribute so the user must provide a password. We could use minlength to set a minimum length for the password, but we have a better idea.

As this brilliant xkcd comic makes clear, a better approach to passwords is to use four random words. To this end, we have used the pattern attribute on the password input. It requires the password to be four or more space-separated words of four or more characters each.

The pattern: [a-zA-Z]{4,}( [a-zA-Z]{4,}){3,}.

Of course, we make this requirement clear with a help message above the input. There a screen reader will announce it before entering the field. And to be extra certain, we associate it with the input via the aria-labelledby and id attributes as shown.

Hereʼs what that looks like:

Submitting the form with a bad email address raises the message, “Please include an '@' in the email address.”

Here are the possibilities:

  • Empty input (required): “Please fill out this field.”
  • bob@: “Please enter a part following '@'. 'bob@' is incomplete.”
  • @dobbs: “Please enter a part followed by '@'. '@dobbs' is incomplete.”
  • bob.dobbs: “Please include an '@' in the email address. 'bob.dobbs' is missing an '@'.” (As above.)

Here is what happens if the password doesnʼt match our pattern:

Submitting the form with a bad password raises the message, “Please match the requested format.”

Here are the possibilities:

  • Empty input (required): “Please fill out this field.”
  • Bob is yer uncle: “Please match the requested format.”

Be sure that you explain what the “requested format” is! Donʼt make your users guess!

Try “correct horse battery staple”. Does it work? xkcd would be proud.

Give this simple form a try.

What else can we validate?

Different types of input provide different validations. Here are some examples:

  • Pattern mismatch: You can use the pattern attribute with other input types as well. These include: email, search, tel, text, and url.
  • Range overflow/underflow: You can set max and min values on types date, datetime-local, month, number, range, time, and week.
  • Step mismatch: You can set the step value (a number) on types date, datetime-local, month, number, range, time, and week. Sheesh! Who knew there were so many input types? MDN provides a handy list of default values.
  • Too long/short: You can set a maxlength and/or minlength (as a number of characters) on types email, password, search, tel, text, and url.

Donʼt try to memorize these. Remember: code (and learn) just in time. There is no point in wasting time and effort that you may never need.

Instead, first design your form. Then determine what needs validation. Finally, refer to Mozilla Developer Network or equivalent to see what fits your needs.

For example, you might create an integer input like this:

<input
  id="iq"
  max="210"
  min="0"
  name="iq"
  required
  step="1"
  type="number"
>
Enter fullscreen mode Exit fullscreen mode

This accepts only positive integers between 0 and 210 (the highest IQ on record). The step value of 1 does not prevent entering decimals, such as 100.1. But try submitting that. Youʼll get a warning:

“Please enter a valid value. The two nearest valid values are 100 and 101.”

You can try it on our example form. What happens if you enter -50? What about 300?

You could also do this with an input of type text by setting the pattern attribute to [0-9]*. Or use ([0-9]|[1-9][0-9]*)? if you want to disallow starting zeros. But either way you lose the semantic value of the number type.

Our recommendation: stick with the number input.

The key takeaway here is this: good enough is, by definition, good enough.

Too often, designers and devs create bloated, ugly, brittle, incomprehensible code. We do it because we want to tweak our interface in some minor way, but plain HTML doesnʼt make it easy. So we hack the heck out of it.

The temptation to throw out all the benefits of semantic, accessible, browser-native code just to make it a bit slicker can be overwhelming. But resist, resist, resist!

This isnʼt about UX. Your users donʼt care about your flashy interface, no matter what they tell you when you ask. Thatʼs your ego talking.

Users care about usability, findability, comprehensibility, accessibility.

Can they find what they are looking for? Can they understand it when they find it? Can they make your site do what they want it to do?

And we can manage all that with an elegant design without having to hack the code. Simple and beautiful. And more stable, less likely to be buggy, and be easier to code and refactor, too.

And if they never see your flashy, edgy new design, they will never miss it.

Top comments (0)