Give every field a visible label, add the right autocomplete token and input type, make error and confirmation messages announce themselves, and make the button say what will happen. A console script can check those on any form; on a typical placeholder-only form it found 15 problems.

Key takeaways

  • Placeholders are not labels. They vanish when you type, and WCAG 1.3.1 and 3.3.2 ask for labels or instructions that persist.
  • Autocomplete saves typing and is a WCAG requirement. Success criterion 1.3.5 asks for the input purpose to be identifiable for common personal-data fields.
  • The right input type opens the right keyboard. type="email" and type="tel" help on phones.
  • Errors need identifying and describing in text. WCAG 3.3.1 asks for the field in error to be identified and the error described in text, and 4.1.3 for status messages to be exposed to assistive technology.
  • Ask for less. Every extra field is a reason to leave; the form that needs a name, a way to reply and a message should ask for exactly those.

The rules behind the audit

Checks and the WCAG 2.2 criteria they come from.
CheckWhyStatus
Every field has a label1.3.1 Info and Relationships, 3.3.2 Labels or Instructions, 4.1.2 Name, Role, ValueDocumented
Autocomplete on name, email, phone, address1.3.5 Identify Input PurposeDocumented
Errors identified and described in text3.3.1 Error IdentificationDocumented
Messages announced without moving focus4.1.3 Status MessagesDocumented
Targets at least 24 by 24 CSS pixels (or spaced)2.5.8 Target Size (Minimum)Documented
Button says what happens ("Send message")Clarity, not a WCAG requirementPractice
Email and phone use the matching input typeMobile keyboards, built-in validationPractice

Source: WCAG 2.2.

The audit script

Paste it in the browser console on the page, then run auditForms():

function auditForms() {
  const HINTS = { name: 'name', fullname: 'name', email: 'email', tel: 'tel', phone: 'tel', address: 'street-address', postcode: 'postal-code', zip: 'postal-code', city: 'address-level2', org: 'organization', company: 'organization' };
  const report = [];
  document.querySelectorAll('form').forEach((form, fi) => {
    const issues = [];
    form.querySelectorAll('input, select, textarea').forEach(el => {
      if (['hidden', 'submit', 'button', 'checkbox', 'radio'].includes(el.type)) return;
      const id = el.name || el.id || el.type;
      const labelled = el.labels && el.labels.length || el.getAttribute('aria-label') || el.getAttribute('aria-labelledby');
      if (!labelled) issues.push(id + ': no label' + (el.placeholder ? ' (placeholder only)' : ''));
      const key = Object.keys(HINTS).find(k => (el.name + ' ' + el.id).toLowerCase().includes(k));
      if (key && !el.autocomplete) issues.push(id + ': add autocomplete="' + HINTS[key] + '"');
      if (/email/i.test(el.name + el.id) && el.type !== 'email') issues.push(id + ': use type="email"');
      if (/(tel|phone)/i.test(el.name + el.id) && el.type !== 'tel') issues.push(id + ': use type="tel"');
      const h = el.getBoundingClientRect().height; if (h && h < 24) issues.push(id + ': ' + Math.round(h) + 'px tall (under 24px)');
    });
    form.querySelectorAll('button, input[type=submit]').forEach(b => {
      const t = (b.textContent || b.value).trim();
      if (/^(submit|send|go|ok)$/i.test(t)) issues.push('button "' + t + '": say what happens');
      const r = b.getBoundingClientRect(); if (r.height && r.height < 24) issues.push('button "' + t + '": ' + Math.round(r.height) + 'px tall');
    });
    if (!form.querySelector('[aria-live], [role=alert], [role=status]') && !form.parentElement.querySelector('[aria-live], [role=alert], [role=status]')) issues.push('no live region: errors and confirmations may not be announced');
    report.push({ form: fi + 1, id: form.id || form.getAttribute('data-signup') !== null && 'data-signup' || '(none)', issues });
  });
  return report;
}

Results

On a deliberately typical bad contact form (placeholder-only name, email, phone and message fields, a button labelled "Submit", no message area) it reported 15 issues: four missing labels, three missing autocomplete tokens, two wrong input types, four undersized targets, a vague button and no live region.

On this site's forms it found one real problem. The newsletter form's confirmation message was not in a live region, so a screen reader would not announce success or failure. I added role="status" to it on every page, and the audit now reports no issues for it. The audit also flagged the three range sliders in the hosting calculator as 5 px tall, which is a false alarm: the slider track is thin by design and the draggable handle is larger. I mention it because scripts like this overreport and you need to read each line.

Beyond the audit

  • Keep the visible label above the field, not inside it; it stays readable while typing.
  • Validate on submit, not on every keystroke, and put the message beside the field and in a summary the screen reader can reach.
  • Keep what the user typed when an error occurs.
  • Say what happens next after sending: when they will hear back, and where to go if they do not.
  • Test it with the keyboard alone, which is part of the 10-minute accessibility test.

What this does not cover

An audit finds structure problems, not reasons for abandonment. The script checks markup and size, not wording, trust, field count or whether a CAPTCHA drives people away, and it cannot see what happens after submit. The bad form was written to contain the problems, so the count of 15 describes the script's coverage, not how common the problems are. I have no analytics on abandonment for any form, so I make no claim about conversion gains.

Badri Dutta

Software engineer · 8 years technical SEO

Written after the audit found a genuine gap in this site's own newsletter form and a false alarm in its calculator. I report both because the script overreports.

Full background →