Sophie Au

Software Developer, Web Designer, Tea Enthusiast

Accessibility on the Web: Forms

31 May 2020

First things first: Please use forms when appropriate. Don't just chuck in p, an input element and a button. Make them connected. That makes everyone's lives easier: People using screen readers, people who like to tab through forms and even mobile users who will be able to auto-focus on the next form field on enter. Also: Built-in input validation! Who does't like that?

But this post isn't about the advantages of using semantic forms. For that, check out the MDN Docs on forms.

Okay, now you've got an input and a button inside a <form> element. But just wrapping them inside the <form> doesn't make them accessible.

Use the Correct Input Type

A simple one but easy to forget about. When you have an <input />, set a type. There are quite a few to choose from so it's very likely that one of them will fit your use case. Not only do they help out with validation, formatting, announcing the right thing to the screen reader, the type also decides, which keyboard will be shown on mobile. email will bring up the keyboard with @ and such on the first screen and number will bring up the numeric keyboard.

While we're talking about number: Please remember that type="number" is really only for numbers that you'll want to increment. For things like German Postal Codes and credit card numbers please use:

<input type="text" inputmode="numeric" pattern="[0-9]*">

It will still display the right keyboard on mobile but act a lot more like a 'normal' text input.

Also, please do not use multpile input fields for one value just becaues it looks nice. A credit card input field should be 1 input, not 4!

Label Your Inputs

Say it with me "Every Input needs a label" No, a placeholder doesn't cut it. Neither does a disconnected span or p. You need to properly label your inputs. If it's too ugly visually hide the label. But please, please, give every input a label. Here's how to correcly label an input:

<label for="name">Name:</label>
<input id="name" />

You can also use aria-label to label the input but there better be a really really good reason for not using a normal <label> element.

Describe Your Inputs

Labelling inputs is really just the lowest rung of the ladder. Quite often it makes sense to add an additional description to your input. E.g. making the user aware that a password needs to have a length of at least 6 characters. Or that there was an error in their input.

For these cases, use aria-describedby and point it to the id of the description.

Announce Invalid States

When you have a custom error in yout input (i.e. the input failed your custom validation), set the aria-invalid attribute. That way, the error will be announced to screen readers.

Other Attributes

Use the name attribute. It's super-useful when you're using the built-in form API for submission and validation.

And if you're using radiobutton as an input type you really don't have a choice. Only radio buttons with the same name are treated as different options for the same field. If you don't set a name, each radio button will be considered completely independent from the others.

Also, mark fields as required in the tag. Don't do it by using pure js on submission or by showing an asterisk next to the label.

The same goes for validation. Don't use your custom validation if the API gives you so much to work with out of the box. Inputs in general have attributes such as minlength or pattern or even just the type which allow you to do validation without actually having to use javascript. Just be aware that validation vie pattern doesn't prevent users from adding invalid characters. It will just prevent them from submitting them.

And if you have closely related data that needs to have separate input fields, use a fieldset.

The Submit Button

Add a submit button to your form! Otherwise you'll potentially have implicit submission, i.e. the form gets submitted when you press enter in the last field.

<button type="submit">Submit</button>

Don't vs Please Do

As a send-off here is an example of what not to do and what to do instead:

const CreditCardForm = () => {
  const [ccn, setCCN] = useState(['', '', '', '']);

  const handleCCNChange = (index) => (e) => {
    const newValue =\D/g, '');
    if (newValue.length > 4) return;

    const newCCN = [...ccn];
    newCCN[index] = newValue;

  const submitCC = async () => {
    await sendCCN(ccn.join(''));

  return (
      <p>Your credit card number here:</p>
      <input onChange={handleCCNChange(0)} value={ccn[0]} />
      <input onChange={handleCCNChange(1)} value={ccn[1]} />
      <input onChange={handleCCNChange(2)} value={ccn[2]} />
      <input onChange={handleCCNChange(3)} value={ccn[3]} />
      <button onClick={submitCC}>Submit</button>

const CreditCardForm = () => {
 const [ccn, setCCN] = useState('');

  const handleCCNChange = (e) => {
    const newValue =\D/g, '');

  const submitCC = async () => {

  return (
      <label htmlFor="cc-input">Your credit card number here:</label>
      <button type="submit" onClick={submitCC}>


Geri Reid did an amazing write-up that is much more extensive and detailed than what I threw together here.