Courses Learn Blog Contact
Get Started →
Lesson 13

Day 13: Forms, UX and Accessibility

Validation people can act on, keyboard operation, focus management, live regions, and the ten-minute audit to run on everything you ship.

Webbo3 Level 2: JavaScript Development 19 min read Free

Webbo3 Level 2 · JavaScript Development · Day 13 of 20

Forms, UX and Accessibility

The day you unplug your mouse. Validation people can actually act on, and an interface that works for everybody who visits it.

By the end of today you can

  • Validate a form and say what is wrong in a way the user can fix
  • Use the browser's own validation instead of rewriting it
  • Operate your entire application with the keyboard alone
  • Manage focus so a keyboard user is never lost
  • Announce changes that happen without a page reload
  • Know when a div should have been a button, and why it matters

This is the lesson most bootcamps skip, and skipping it is why so much of the web is unusable for a lot of people. It is also, increasingly, a legal requirement, and it is one of the few things a junior developer can be noticeably better at than the team they join. An hour today is worth a great deal later.

1. The browser validates for free

HTMLindex.html
<form novalidate>
  <label for="email">Email address</label>
  <input id="email" name="email" type="email" required
         autocomplete="email" aria-describedby="email-error">
  <p id="email-error" class="error" role="alert"></p>

  <label for="phone">Phone</label>
  <input id="phone" name="phone" type="tel" inputmode="numeric"
         pattern="[0-9]{11}" autocomplete="tel">

  <button type="submit">Create account</button>
</form>
AttributeWhat it buys you
type="email"Validation, and the right keyboard on a phone
type="tel" + inputmode="numeric"A number pad instead of a keyboard
requiredThe browser blocks submission and focuses the field
patternA regular expression check with no JavaScript
autocompleteThe browser fills it in: a huge usability win
novalidateTurn the browser's own messages off so you can write better ones

The Constraint Validation API then lets you keep the browser's checking and replace only its wording, which is the best of both.

JSapp.js
form.addEventListener('submit', (event) => {
  event.preventDefault();
  let firstBad = null;

  for (const field of form.elements) {
    if (!field.name) continue;
    const error = document.querySelector(`#${field.name}-error`);
    if (field.validity.valid) {
      field.removeAttribute('aria-invalid');
      if (error) error.textContent = '';
      continue;
    }
    field.setAttribute('aria-invalid', 'true');
    if (error) error.textContent = messageFor(field);
    firstBad = firstBad ?? field;
  }

  if (firstBad) { firstBad.focus(); return; }    // send them to the problem
  submit();
});

function messageFor(field) {
  if (field.validity.valueMissing) return `${label(field)} is required.`;
  if (field.validity.typeMismatch) return `That does not look like an email address.`;
  if (field.validity.patternMismatch) return `Enter 11 digits, no spaces.`;
  return 'Please check this field.';
}

Focusing the first bad field is not a nicety

On a long form on a phone, an error message eight fields above the fold is an error message nobody sees. They press submit, nothing appears to happen, and they leave. firstBad.focus() is two words and it is the difference between a form that gets completed and one that gets abandoned.

Error messages that help

Instead ofWrite
Invalid inputEnter 11 digits, with no spaces
ErrorThat email is already registered. Sign in instead?
Password too weakAdd one number. Passwords need 8 characters and a digit
Required fieldWe need your phone number to confirm the delivery

The test: does the message tell them what to do next? "Invalid" describes your code's opinion. "Enter 11 digits" describes their next action.

2. Unplug your mouse

Do this before reading further

Open your Day 5 or Day 9 project. Put the mouse away, genuinely. Now use the entire application with Tab, Shift+Tab, Enter, Space and the arrow keys. Write down every place you get stuck. Most people find three or four in five minutes, and it is a sobering exercise the first time.

KeyShould do
TabMove to the next interactive thing, in a sensible order
Shift+TabMove back
EnterActivate a link or a button, submit a form
SpaceActivate a button, tick a checkbox, scroll
EscapeClose the modal or the dropdown
ArrowsMove within a radio group, a menu or a slider

3. The div-that-should-be-a-button

JSapp.js
<!-- looks like a button. Is not one. -->
<div class="btn" onclick="save()">Save</div>

<!-- is a button -->
<button type="button" class="btn">Save</button>

That div cannot be focused with Tab, does not respond to Enter or Space, is not announced as a button by a screen reader, and shows no focus ring. To make it behave you would have to add tabindex="0", a role, and keyboard handlers for two keys: four things the <button> element already does. Use the right element and accessibility is mostly free.

If itUse
Goes to another page or URL<a href="...">
Does something on this page<button type="button">
Submits the form<button type="submit">
Is a form control<input>, <select>, <textarea>, with a <label>
Is just a box<div>

The default button type is submit

A <button> inside a form with no type attribute submits the form. Put a Delete button inside a form without type="button" and clicking it reloads the page. This is a genuinely common bug and the fix is one word.

4. Focus, and where it goes

When a modal opens, focus must go into it. When it closes, focus must go back to whatever opened it. Otherwise a keyboard user closes a dialog and finds themselves back at the top of the document with no idea where they are.

JSapp.js
let lastFocused = null;

function openDialog() {
  lastFocused = document.activeElement;      // remember where we came from
  dialog.hidden = false;
  dialog.querySelector('button, [href], input, select, textarea')?.focus();
  document.addEventListener('keydown', onEscape);
}

function closeDialog() {
  dialog.hidden = true;
  document.removeEventListener('keydown', onEscape);
  lastFocused?.focus();                       // put them back
}

const onEscape = (event) => { if (event.key === 'Escape') closeDialog(); };

Never remove the focus ring

outline: none is in more stylesheets than almost any other rule, and it makes an interface impossible to navigate by keyboard: you cannot see where you are. If the default ring is ugly, replace it with something visible that matches your design. Use :focus-visible so it shows for keyboard users without appearing on every mouse click.

CSScss/style.css
/* never this */
button:focus { outline: none; }

/* this */
button:focus-visible {
  outline: 3px solid #0a6e6e;
  outline-offset: 2px;
}

5. Telling a screen reader what changed

When you update the page with JavaScript, a sighted user sees it. A screen reader user is told nothing at all, because nothing was navigated to. A live region fixes that: anything that appears inside it is announced.

JSapp.js
<!-- polite: waits for a pause. Right for search result counts -->
<p id="status" role="status" aria-live="polite"></p>

<!-- assertive: interrupts. Only for genuine errors -->
<p id="error" role="alert"></p>
JSapp.js
status.textContent = `${results.length} students found`;
  • The element must already be in the DOM when the page loads. Creating the live region and its content at the same moment announces nothing.
  • role="status" waits for a natural pause. role="alert" interrupts, so save it for errors: an interface that interrupts constantly is unusable.

6. The ten-minute audit

Run this on every project you ship from now on. It is not comprehensive, and it catches most of what a beginner gets wrong.

  1. Tab through it. Can you reach everything, in a sensible order, and always see where you are?
  2. Every image: does the alt say what it shows, or is it alt="" because it is decorative? "image1.jpg" is worse than nothing.
  3. Every input: does clicking its label focus the field? If not, for and id do not match.
  4. Headings: h1 then h2 then h3, in order, no skipping. A screen reader user navigates by them the way you navigate by eye.
  5. Colour contrast: 4.5:1 for body text, as you learned last month. DevTools shows you the ratio when you inspect a colour.
  6. Colour alone: is any information conveyed only by colour? Add an icon or a word for the eight per cent of men who are colour blind.
  7. Zoom to 200%. Does anything overlap or disappear?
  8. Run Lighthouse. DevTools, Lighthouse tab, tick Accessibility. It is not the whole answer and it will find real problems.

Do this now

  1. Do the keyboard audit on your project and write down every problem.
  2. Fix them. Expect the list to include a div that should be a button, a missing label, and a removed focus ring.
  3. Rewrite every validation message so it says what to do next.
  4. Add a live region announcing your result count.
  5. Run Lighthouse and get Accessibility above 95. Then read what it still complains about, because the last few points teach you the most.

Checkpoint

Q1. Why is a clickable `div` worse than a `button`?

It cannot be tabbed to, does not respond to Enter or Space, is not announced as a button, and has no focus ring. The button element does all four without you writing anything.

Q2. A Delete button inside a form reloads the page. Why?

A <button> with no type defaults to type="submit". Add type="button".

Q3. What is wrong with `outline: none`?

It hides the only indication of where keyboard focus is, which makes the interface impossible to navigate without a mouse. Replace the ring, do not remove it, and use :focus-visible.

Q4. Why does focus need to return when a dialog closes?

Because otherwise focus falls back to the top of the document and the keyboard user loses their place entirely, with no visual cue about what happened.

Tick before you move on

  • I used my whole application with no mouse and fixed everything I found
  • Every interactive element is the correct HTML element
  • Focus is visible everywhere and returns correctly after a dialog
  • Every validation message says what to do next
  • Lighthouse Accessibility is above 95

Quick recap

The browser validates for free; replace only its wording, not its checking · Focus the first invalid field, or the error is never seen · Right element, free accessibility: button for actions, a for navigation · Never remove the focus ring, replace it, and use :focus-visible · Live regions announce what a sighted user can simply see

Tomorrow, Day 14: finding bugs instead of guessing at them, and the difference between code that works and code that is fast.

Learn this with a real instructor

Free lessons take you a long way on your own. The full programmes add live classes, assignments, Skill Points and a certificate — built for African students.