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 11 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 act on, and an interface that works for everybody who visits it.

By the end of today you can

  • Use the browser's own validation, and replace only its wording
  • Write error messages that tell people what to do next
  • Operate your whole application with the keyboard alone
  • Choose the right element, so accessibility comes free
  • Manage focus, so a keyboard user is never lost
  • Announce changes that happen without a page reload

This is the lesson most bootcamps skip, and skipping it is why so much of the web is unusable for a lot of people.

Accessibility means building so that everyone can use what you make: people using a keyboard instead of a mouse, people using a screen reader (software that reads the page aloud for blind users), people who zoom in, or who cannot tell red from green. It is 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.

1. The Browser Validates for Free

Definition

HTML attributes such as required, type="email" and pattern make the browser check a field before the form is sent. The Constraint Validation API lets your JavaScript read those checks, so you keep the browser's checking and write only better wording.

Real-Life Example

A bank form already has boxes marked "required" and "digits only". The bank clerk does not invent new rules; they just explain the existing ones kindly when you get one wrong.

Types: The Attributes

AttributeWhat it gives you
type="email"An email check, and the right keyboard on a phone
type="tel" inputmode="numeric"A number pad instead of a full keyboard
requiredThe field cannot be left empty
pattern="[0-9]{11}"A shape the value must match: here, exactly 11 digits
autocomplete="email"The browser can fill it in: a huge usability win
novalidateOn the form: turn off the browser's own pop-ups so you can show better ones

How the API Works

Each field has field.validity, an object of yes-or-no answers such as valueMissing (empty but required), typeMismatch (not an email) and patternMismatch (does not match the pattern). validity.valid is true when everything passes. form.elements is every field in the form.

aria-invalid="true" tells a screen reader the field has a problem, and field.labels[0] is the label attached to it.

What this code doesWe want the browser to do the checking and our code to do the explaining. The user types a broken email and a phone number with a space, then presses Create account.
index.html
1
2
3
4
5
6
7
8
9
10
11
<form id="signup" novalidate>
  <label for="email">Email address</label>
  <input id="email" name="email" type="email" required autocomplete="email">
  <p id="email-error" class="error"></p>

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

  <button type="submit">Create account</button>
</form>
style.css
1
2
label { display: block; margin-top: 6px; }
.error { color: #b3261e; margin: 2px 0; }
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const form = document.querySelector('#signup');

// Turn the browser's verdict into words the user can act on
function messageFor(field) {
  const label = field.labels[0].textContent;
  if (field.validity.valueMissing) return `${label} is required.`;
  if (field.validity.typeMismatch) return 'That does not look like an email address.';
  if (field.validity.patternMismatch) return 'Enter 11 digits, with no spaces.';
  return 'Please check this field.';
}

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

  // Check every named field with the browser's own rules
  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');
      error.textContent = '';
      continue;
    }

    field.setAttribute('aria-invalid', 'true');
    error.textContent = messageFor(field);
    firstBad = firstBad ?? field;
  }

  // Send the user straight to the first problem
  if (firstBad) {
    firstBad.focus();
    console.log('focus moved to:', document.activeElement.id);
    return;
  }
  console.log('all valid, sending');
});
BROWSERindex.html · after submitting with two mistakes
CONSOLE   BROWSER DEVTOOLS
focus moved to: email
What this code doesWe want to see the same form when both fields are right.
index.html
1
2
3
4
5
6
7
8
9
10
11
<form id="signup" novalidate>
  <label for="email">Email address</label>
  <input id="email" name="email" type="email" required autocomplete="email">
  <p id="email-error" class="error"></p>

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

  <button type="submit">Create account</button>
</form>
style.css
1
2
label { display: block; margin-top: 6px; }
.error { color: #b3261e; margin: 2px 0; }
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
const form = document.querySelector('#signup');

// Turn the browser's verdict into words the user can act on
function messageFor(field) {
  const label = field.labels[0].textContent;
  if (field.validity.valueMissing) return `${label} is required.`;
  if (field.validity.typeMismatch) return 'That does not look like an email address.';
  if (field.validity.patternMismatch) return 'Enter 11 digits, with no spaces.';
  return 'Please check this field.';
}

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

  // Check every named field with the browser's own rules
  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');
      error.textContent = '';
      continue;
    }

    field.setAttribute('aria-invalid', 'true');
    error.textContent = messageFor(field);
    firstBad = firstBad ?? field;
  }

  // Send the user straight to the first problem
  if (firstBad) {
    firstBad.focus();
    console.log('focus moved to:', document.activeElement.id);
    return;
  }
  console.log('all valid, sending');
});
BROWSERindex.html · after submitting valid details
CONSOLE   BROWSER DEVTOOLS
all valid, sending
Focusing the First Bad Field Is Not a Nicety

On a long form on a phone, an error eight fields above the screen is an error nobody sees. They press submit, nothing seems to happen, and they leave. firstBad.focus() is the difference between a form that gets finished 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.

Picture This

Picture the kind bank clerk: the rules are printed on the form already, and the clerk's job is to point at the exact box and say what to write there.

2. Unplug Your Mouse

Definition

Keyboard accessibility means everything in your app can be reached and used with the keyboard alone, in a sensible order, while always showing where you are.

Real-Life Example

A lift with buttons only at the top of the wall, too high to reach from a wheelchair. It works perfectly for most people and not at all for some. A mouse-only interface is that lift.

KeyShould do
TabMove to the next interactive thing, in a sensible order
Shift+TabMove back
EnterFollow a link, press a button, submit a form
SpacePress a button, tick a checkbox
EscapeClose the dialog or dropdown
Arrow keysMove within a group of options, a menu or a slider
Do This Before Reading Further

Open your Day 5 or Day 9 project. Put the mouse away, genuinely. Use the whole app 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.

Picture This

Picture the lift with buttons out of reach. Every mouse-only control is a button someone cannot press.

3. The div That Should Have Been a Button

Definition

Every HTML element has a built-in role. A <button> can be focused with Tab, pressed with Enter or Space, is announced as a button by screen readers, and shows a focus ring. A <div> is just a box and does none of that, even if you attach a click listener to it.

Real-Life Example

A painted picture of a door on a wall. It looks like a door, and if you are standing right in front of it you might even push it, but it does not open, and a blind visitor feeling along the wall has no idea it is meant to be a door.

What this code doesWe want to see what the keyboard does with a clickable div and a real button side by side. The user presses Tab once, then Enter.
index.html
1
2
<div class="fake">Save (div)</div>
<button class="real">Save (button)</button>
style.css
1
.fake { display: inline-block; padding: 6px 12px; background-color: #eee; cursor: pointer; }
app.js
1
2
3
4
5
6
7
8
// Both have a click listener
document.querySelector('.fake').addEventListener('click', () => console.log('div clicked'));
document.querySelector('.real').addEventListener('click', () => console.log('button clicked'));

// Report where the keyboard focus is after each key
document.addEventListener('keyup', (event) => {
  console.log(`after ${event.key}, focus is on: ${document.activeElement.textContent}`);
});
BROWSERindex.html · after pressing Tab, then Enter
CONSOLE   BROWSER DEVTOOLS
after Tab, focus is on: Save (button)
button clicked
after Enter, focus is on: Save (button)

The first Tab went straight past the div to the button, and Enter pressed the button. For a keyboard user, the div does not exist.

Types: Choosing the Right Element

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> or <textarea>, with a <label>
Is just a box for layout<div>

Common Mistake: The Default Button Type Is Submit

A <button> inside a form with no type is a submit button. Put a Delete button inside a form without type="button" and clicking it submits the form.

What this code doesWe want to see what a Delete button with no type does inside a form, compared with one that says type="button".
index.html
1
2
3
4
5
<form id="edit">
  <input value="Read Day 13">
  <button class="no-type">Delete (no type)</button>
  <button class="typed" type="button">Delete (type="button")</button>
</form>
app.js
1
2
3
4
5
6
7
8
9
// Report whenever the form is submitted
document.querySelector('#edit').addEventListener('submit', (event) => {
  event.preventDefault();
  console.log('the FORM was submitted');
});

// Each Delete button's own action
document.querySelector('.no-type').addEventListener('click', () => console.log('delete (no type) clicked'));
document.querySelector('.typed').addEventListener('click', () => console.log('delete (typed) clicked'));
BROWSERindex.html · after clicking both Delete buttons
CONSOLE   BROWSER DEVTOOLS
delete (no type) clicked
the FORM was submitted
delete (typed) clicked

Only the button with no type also submitted the form. Without our preventDefault, that submission would have reloaded the page. The fix is one attribute.

Picture This

Picture the painted door: it may look right, but only a real door opens, for everyone. Use the real element and most of accessibility is done for you.

4. Focus, and Where It Goes

Definition

Focus is where the keyboard is pointing right now: document.activeElement. When a dialog opens, focus must move into it; when it closes, focus must go back to whatever opened it.

Real-Life Example

A waiter shows you to a side room to sign something, then walks you back to your own table. If they left you at the restaurant entrance instead, you would have to find your table all over again.

What this code doesWe want a dialog that takes focus when it opens, closes on Escape, and gives focus back to the button that opened it. The user tabs to the button, presses Enter, then Escape.
index.html
1
2
3
4
5
6
<button id="open">Edit profile</button>
<div id="dialog" hidden>
  <label for="name">Name</label>
  <input id="name" value="Chidinma">
  <button id="close">Close</button>
</div>
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const dialog = document.querySelector('#dialog');
let lastFocused = null;

function openDialog() {
  // Remember where the user came from
  lastFocused = document.activeElement;
  dialog.hidden = false;
  // Move focus into the dialog
  document.querySelector('#name').focus();
  document.addEventListener('keydown', onEscape);
  console.log('opened; focus on:', document.activeElement.id);
}

function closeDialog() {
  dialog.hidden = true;
  document.removeEventListener('keydown', onEscape);
  // Put the user back where they were
  lastFocused.focus();
  console.log('closed; focus back on:', document.activeElement.id);
}

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

document.querySelector('#open').addEventListener('click', openDialog);
document.querySelector('#close').addEventListener('click', closeDialog);
BROWSERindex.html · after Tab, Enter, then Escape
CONSOLE   BROWSER DEVTOOLS
opened; focus on: name
closed; focus back on: open
Never Remove the Focus Ring outline: none is in more stylesheets than almost any other rule, and it makes an interface impossible to use by keyboard: you cannot see where you are. If the default ring is ugly, replace it with a visible one that matches your design, using :focus-visible so it shows for keyboard users without appearing on every mouse click.
What this code doesWe want a focus ring that matches the design and is always visible to keyboard users.
style.css
1
2
3
4
5
6
7
8
/* Never this: it hides where the keyboard is */
button:focus { outline: none; }

/* This: a clear ring, shown to keyboard users */
button:focus-visible {
  outline: 3px solid #0a6e6e;
  outline-offset: 2px;
}
NOTE   
Read by the browser whenever an element gets keyboard focus.
Picture This

Picture the waiter walking you back to your own table. A keyboard user must always end up back where they were, and always see where they are.

5. Telling a Screen Reader What Changed

Definition

A live region is an element with role="status" (or aria-live). Screen readers read out anything that appears inside it, so a blind user hears about changes a sighted user simply sees.

Real-Life Example

An announcement over the speakers at a train station: "The 3:15 to Ibadan is now at platform 2." Passengers who cannot see the board still find out.

How It Works

  1. The element must already be on the page when it loads. Creating it and its message at the same moment announces nothing.
  2. role="status" waits for a natural pause before speaking. role="alert" interrupts, so save it for errors: an interface that interrupts constantly is unusable.
  3. Update its text, and the screen reader reads the new text.

aria-label gives the search box a name for screen readers when there is no visible label.

What this code doesWe want the result count to be announced to screen reader users every time a search changes it.
index.html
1
2
<input id="search" aria-label="Search students">
<p id="status" role="status"></p>
app.js
1
2
3
4
5
6
7
8
9
10
const students = ['Chidinma', 'Musa', 'Amaka', 'Tunde', 'Zainab'];
const status = document.querySelector('#status');

document.querySelector('#search').addEventListener('input', (event) => {
  const term = event.target.value.toLowerCase();
  const found = students.filter((name) => name.toLowerCase().includes(term));

  // Everything written here is announced by screen readers
  status.textContent = `${found.length} students found`;
});
BROWSERindex.html · after typing "a"
Picture This

Picture the station announcement: the board changes for those who can see it, and the speakers say the same thing for those who cannot.

6. The Ten-Minute Audit

Run this on every project you ship from now on. It is not everything, 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 text say what it shows, or is it alt="" because it is only decoration? "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 go h1, then h2, then h3, in order with no skipping. Screen reader users navigate by them.
  5. Colour contrast is at least 4.5 to 1 for body text. DevTools shows the ratio when you inspect a colour.
  6. Nothing is shown by colour alone. Add an icon or a word for the roughly one in twelve 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 finds 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 reached with Tab, does not respond to Enter or Space, is not announced as a button, and has no focus ring. The button element does all four for free.

Q2. A Delete button inside a form submits the form. Why?

A button with no type is type="submit". Add type="button".

Q3. What is wrong with outline: none?

It hides the only sign of where keyboard focus is. Replace the ring rather than removing it, using :focus-visible.

Q4. Why must focus return when a dialog closes?

Otherwise it falls back to the top of the page and the keyboard user loses their place entirely.

Tick Before You Move On

  • ☐ I used my whole app with no mouse and fixed everything I found
  • ☐ Every interactive element is the correct HTML element
  • ☐ Every button inside a form has a type
  • ☐ Focus is visible everywhere and returns correctly after a dialog
  • ☐ Every validation message says what to do next, and focus goes to the first problem
  • ☐ 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. The right element gives you accessibility for free: button for actions, a for navigation. A button with no type submits its form. Never remove the focus ring: replace it, with :focus-visible. Live regions announce what a sighted user can simply see.

Final Scenario

A student using a screen reader signs up for your course, enrols, and searches the resource list, without once needing a mouse.

The form told them exactly what to fix and took them straight there. Every control was a real button. The dialog gave them back their place. And each search result count was read aloud. They never knew any of that was work; it simply worked.

Accessibility done well is invisible. That is the whole point of doing it.

Tomorrow, Day 14: debugging, testing and performance. 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.