HTML and Semantics skill 2 of 4

HTML Forms SKILL.md: Labels, Autofill and Validation That Work for Everyone

The HTML Forms skill makes Claude, Cursor or another AI agent audit and fix web forms. A read-only checker flags missing labels, invented autocomplete tokens, wrong input types and ungrouped radios. The agent then builds a field table, applies fixed rules for autofill and validation, and never renames the fields your server reads.

html-forms/SKILL.md, version 1.0.0, 4,611 words, plus scripts/forms_check.py.

Forms are where websites lose people. A field with a placeholder and no label, a phone number typed into a type="number" box, an error message that only turns a border red: each one costs someone a sign-up or an order. The WebAIM Million 2026 report found missing form input labels on 51% of one million home pages.

This skill makes an agent audit and fix HTML forms with fixed rules for labels, input types, autofill tokens and validation. It starts with a read-only checker, writes a field-by-field plan, and changes markup in small approved batches without renaming the fields the server depends on. Validation always happens on the server too.

It works on plain HTML and on the rendered output of any framework or form library. Library and browser facts sit in the appendix.

When to use this skill, and when not to

Use it when the user wants to:

  • Audit or fix a sign-up, sign-in, checkout, address, contact or search form.
  • Make a form work with browser autofill and password managers.
  • Decide when to validate, how to show errors, and what the messages should say.
  • Choose the right type, inputmode and autocomplete for each field.
  • Review a pull request or generated form code.

Do not use it for these jobs:

RequestUse instead
Landmarks, headings, buttons and links outside formssemantic-html.md
Cutting fields, layout and copy to raise conversionform-optimisation.md
Custom widgets with no native element (comboboxes, date pickers)A component accessibility skill. This skill uses native controls
Security of authentication flows, password policy, CAPTCHA choiceA security review. This skill covers markup and validation only

Guardrails that apply to every run

  1. Propose, then change. The agent reports and plans first. It edits files only after the user approves a batch, on a branch under version control. It never pushes, merges or deploys.
  2. Never rename name attributes on its own. The server, analytics and saved autofill data read them. A rename needs the user's yes and a matching server change, listed in the plan.
  3. Never submit real forms. Testing happens in a local or test environment with invented test data. The agent does not submit forms on live sites, make payments or create real accounts.
  4. Server-side validation is required. Client-side checks help users and are easy to bypass. If the server does not validate a field, the report says so as a blocker.
  5. Personal data and credentials. Forms collect names, emails, phone numbers, addresses, dates of birth, card numbers and passwords. The agent never asks for real values, never logs or pastes entered values, and uses obviously fake test data such as test@example.com. It never stores or repeats a password, one-time code or card number, even a test one, in the report.
  6. Batches stay small. At most 2 forms or 30 field changes per batch (defaults), each shown as a diff.
  7. Nothing is invented. Thresholds here are labelled defaults. Tool facts carry a source and the date read. Version-dependent facts carry verify before relying on this.
  8. Not legal advice. Consent checkboxes, privacy notices, age checks and accessibility law are legal questions. The agent marks those fields up correctly and routes the wording and the decision to a person.
  9. Scope boundary. The agent refuses to disable autofill on sign-in fields, to remove server-side checks, to add hidden tracking fields, or to change what data a form collects without the user's decision.

Inputs the agent must have before starting

Ask for what is missing in one message and wait. Do not proceed on assumptions.

#InputWhat to ask if missing
1Target"Which forms should I check? File paths or the URL of a test environment."
2Purpose"What is each form for: sign-in, sign-up, checkout, address, contact, search?"
3Stack"Plain HTML, or a framework or form library (React Hook Form, Formik, a server template)? Is there a schema library such as Zod or Yup?"
4Server checks"Does the server validate every field, and can you point me to that code?"
5Error pattern"Does your design system already have error message and error summary components?"
6Countries"Which countries do you take addresses and phone numbers from?"
7Dependants"Do analytics, tests or the server read field names or IDs?"
8Test setup"Is there a test environment where I can submit with fake data, and can you run Python 3 locally?"

If input 4 is unknown, the agent flags "server validation unconfirmed" at the top of the report and continues with markup only.

Raw SKILL.md file

.md

This is the exact file agents read, including the credit header and licence notice. Save it as html-forms/SKILL.md.

---
name: html-forms
description: "Audits and fixes HTML forms so labels, input types, autofill and validation work for every user, without renaming the field names a server depends on. Runs a read-only standard-library checker that flags missing labels, placeholder-only fields, invented or missing autocomplete tokens, wrong email, phone and number types, radio groups without fieldset, autocomplete off on sign-in fields and buttons without a type, then builds a field table and applies fixed rules for labels, hints, autocomplete, validation timing, error messages and error summaries, with server-side validation required. Use whenever someone asks to fix or review a sign-in, sign-up, checkout, address or contact form, make autofill or password managers work, choose input types, write form error messages or decide when to validate. Do NOT use for landmarks, headings or links (use semantic-html), conversion copy or cutting fields (use form-optimisation), custom date pickers, authentication security or legal consent wording."
license: CC-BY-4.0
metadata:
  author: Afeez Adebayo
  author_url: https://afeezadebayo.com
  author_linkedin: https://www.linkedin.com/in/afeez-adebayo/
  author_x: https://x.com/za_icon
  source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/html-forms/
  category: frontend-development
  subcategory: html-semantics
  version: "1.0.0"
  created: "2026-10-23"
  updated: "2026-10-23"
---

# HTML Forms: Labels, Autofill and Validation That Work for Everyone

Forms are where websites lose people. A field with a placeholder and no label, a phone number typed into a `type="number"` box, an error message that only turns a border red: each one costs someone a sign-up or an order. The WebAIM Million 2026 report found missing form input labels on 51% of one million home pages.

This skill makes an agent audit and fix HTML forms with fixed rules for labels, input types, autofill tokens and validation. It starts with a read-only checker, writes a field-by-field plan, and changes markup in small approved batches without renaming the fields the server depends on. Validation always happens on the server too.

It works on plain HTML and on the rendered output of any framework or form library. Library and browser facts sit in the appendix.

## When to use this skill, and when not to

Use it when the user wants to:

- Audit or fix a sign-up, sign-in, checkout, address, contact or search form.
- Make a form work with browser autofill and password managers.
- Decide when to validate, how to show errors, and what the messages should say.
- Choose the right `type`, `inputmode` and `autocomplete` for each field.
- Review a pull request or generated form code.

Do not use it for these jobs:

| Request | Use instead |
|---|---|
| Landmarks, headings, buttons and links outside forms | `semantic-html.md` |
| Cutting fields, layout and copy to raise conversion | `form-optimisation.md` |
| Custom widgets with no native element (comboboxes, date pickers) | A component accessibility skill. This skill uses native controls |
| Security of authentication flows, password policy, CAPTCHA choice | A security review. This skill covers markup and validation only |

## Guardrails that apply to every run

1. **Propose, then change.** The agent reports and plans first. It edits files only after the user approves a batch, on a branch under version control. It never pushes, merges or deploys.
2. **Never rename `name` attributes on its own.** The server, analytics and saved autofill data read them. A rename needs the user's yes and a matching server change, listed in the plan.
3. **Never submit real forms.** Testing happens in a local or test environment with invented test data. The agent does not submit forms on live sites, make payments or create real accounts.
4. **Server-side validation is required.** Client-side checks help users and are easy to bypass. If the server does not validate a field, the report says so as a blocker.
5. **Personal data and credentials.** Forms collect names, emails, phone numbers, addresses, dates of birth, card numbers and passwords. The agent never asks for real values, never logs or pastes entered values, and uses obviously fake test data such as `test@example.com`. It never stores or repeats a password, one-time code or card number, even a test one, in the report.
6. **Batches stay small.** At most 2 forms or 30 field changes per batch (defaults), each shown as a diff.
7. **Nothing is invented.** Thresholds here are labelled defaults. Tool facts carry a source and the date read. Version-dependent facts carry **verify before relying on this**.
8. **Not legal advice.** Consent checkboxes, privacy notices, age checks and accessibility law are legal questions. The agent marks those fields up correctly and routes the wording and the decision to a person.
9. **Scope boundary.** The agent refuses to disable autofill on sign-in fields, to remove server-side checks, to add hidden tracking fields, or to change what data a form collects without the user's decision.

## Inputs the agent must have before starting

Ask for what is missing in one message and wait. Do not proceed on assumptions.

| # | Input | What to ask if missing |
|---|---|---|
| 1 | Target | "Which forms should I check? File paths or the URL of a test environment." |
| 2 | Purpose | "What is each form for: sign-in, sign-up, checkout, address, contact, search?" |
| 3 | Stack | "Plain HTML, or a framework or form library (React Hook Form, Formik, a server template)? Is there a schema library such as Zod or Yup?" |
| 4 | Server checks | "Does the server validate every field, and can you point me to that code?" |
| 5 | Error pattern | "Does your design system already have error message and error summary components?" |
| 6 | Countries | "Which countries do you take addresses and phone numbers from?" |
| 7 | Dependants | "Do analytics, tests or the server read field names or IDs?" |
| 8 | Test setup | "Is there a test environment where I can submit with fake data, and can you run Python 3 locally?" |

If input 4 is unknown, the agent flags "server validation unconfirmed" at the top of the report and continues with markup only.

## Workflow

### Step 0: Confirm scope and safety

1. Confirm inputs 1 to 8 and the branch name.
2. Tell the user the plan: checker, field table, decisions, batches, verification, report.
3. Gate 1: wait for a yes.

### Step 1: Run the checker

1. Save `scripts/forms_check.py` from this skill folder. It uses only the Python standard library, reads files and writes nothing.
2. Run it on rendered HTML where possible: `python3 scripts/forms_check.py form.html`.
3. Keep the output as the baseline.

| Code | Level | Meaning |
|---|---|---|
| `no-label` | error | No wrapping `label`, `label for`, `aria-label` or `aria-labelledby`. Notes when only a placeholder exists |
| `bad-autocomplete` | error | The `autocomplete` value is not a recognised token |
| `group-without-fieldset` | error | Two or more radios or checkboxes with one `name` sit outside a `fieldset` |
| `fieldset-without-legend` | error | A `fieldset` has no `legend` |
| `missing-autocomplete` | warn | The field's name or id suggests personal data and it has no `autocomplete` |
| `type-mismatch` | warn | An email or phone field has the wrong `type` |
| `number-for-identifier` | warn | `type="number"` on a phone, postcode or card field |
| `autocomplete-off-on-login` | warn | `autocomplete="off"` on a password, username or email field |
| `no-name` | warn | The control has no `name`, so it will not submit or autofill |
| `button-without-type` | warn | A form has more than one button and some have no `type` |
| `form-without-submit` | warn | No submit button found |

The checker reads names and IDs to guess a field's purpose. Its guesses are prompts for the field table, not decisions.

### Step 2: Build the field table

Write one row per field, using the template in the Templates section: purpose, visible label, hint, `type`, `inputmode`, `autocomplete`, required or optional, client rule, server rule and error messages. Every later decision fills a cell in this table. A field with an empty server rule is a blocker under guardrail 4.

### Step 3: Labels, hints and grouping

1. Every control has a visible label that stays on screen while typing. A placeholder is not a label: it disappears as soon as the user types.
2. Link the label with `for` and `id`, or wrap the control in the `label`.
3. Put format help in a hint below the label, linked with `aria-describedby`. Do not hide the hint in a placeholder.
4. Group related controls in `fieldset` with a `legend`: radio sets, checkbox sets, date parts, and address blocks that repeat (billing and shipping).
5. Mark required or optional fields consistently. If most fields are required, mark the optional ones "(optional)". State the convention once at the top of the form. Do not rely on colour or an asterisk alone.
6. Ask only for data the transaction needs. The W3C forms tutorial notes that extra fields raise abandonment. If a field looks unneeded, list it for the user to decide. The agent does not remove it.

### Step 4: Types, keyboards and autofill

Use this table. Browsers save and offer values by `name`, and some also use `id`, so keep both meaningful.

| Purpose | `type` | `autocomplete` | Notes |
|---|---|---|---|
| Full name | `text` | `name` | Use `given-name` and `family-name` only if the server needs them split |
| Email | `email` | `email` | Add `spellcheck="false"` |
| Phone | `tel` | `tel` | Keep it in one input. Do not use `type="number"` |
| Username on sign-in | `text` or `email` | `username` | Use `username` even when the username is an email |
| Password on sign-in | `password` | `current-password` | Never `off` |
| Password on sign-up or reset | `password` | `new-password` | State the rules before submission and link them with `aria-describedby` |
| One-time code | `text` | `one-time-code` | Add `inputmode="numeric"` |
| Address line | `text` | `address-line1`, `address-line2` | Or `street-address` in a single `textarea` |
| Town or city | `text` | `address-level2` | |
| Postcode | `text` | `postal-code` | Accept spaces and lower case. Not `type="number"` |
| Country | `select` | `country` or `country-name` | |
| Card number | `text` | `cc-number` | `inputmode="numeric"`, one input |
| Card expiry | `text` or `select` | `cc-exp`, or `cc-exp-month` and `cc-exp-year` | Native `select` if separate |
| Card security code | `text` | `cc-csc` | `inputmode="numeric"` |
| Quantity, age, amount | `number` | none | `type="number"` is for values you count or calculate |

Rules:

1. **Two addresses on one page.** Prefix tokens with `shipping` or `billing`: `shipping postal-code`, `billing postal-code`.
2. **Never disable autofill on sign-in fields.** MDN states that `autocomplete="off"` often will not stop password managers and browsers may ignore it on login fields. Turning it off only hurts the users who can still use it.
3. **Use `off` only for one-off values that are not the user's data**, such as a CAPTCHA answer or a honeypot field.
4. **If a field must not take the user's own saved data** (an admin entering another person's email), use a token the browser does not recognise for `autocomplete` and give `name` and `id` the same unrecognised value. Adam Silver reported in 2021 that this stopped Chrome filling the field. It depends on browser behaviour, so **verify before relying on this** in your target browsers.
5. **Never invent tokens.** A made-up value such as `zip` defeats autofill and fails WCAG 2.2 success criterion 1.3.5, Identify Input Purpose.

### Step 5: Validation

Apply these rules in order.

1. **The server validates every field.** Always. Client checks are for speed and help only. Reuse the project's existing schema or validation library on both sides. A second library lets client and server rules drift apart (the benji forms skill makes the same point).
2. **Accept reasonable formats.** Strip spaces from postcodes, card numbers and phone numbers. Accept upper or lower case. Ignore stray punctuation that does not change the meaning.
3. **Choose one timing model for the whole form.**

| Situation | Model |
|---|---|
| Default | Validate when the user submits. Redisplay the page with every entry kept, an error summary at the top with focus moved to it, `Error: ` at the start of the page `<title>`, and a message beside each field. This is the GOV.UK Design System pattern |
| Feedback that adds information as the user types | Live feedback is fine: character counts, password rules met so far |
| The design system already validates when a field loses focus | Keep it for consistency. Never show an error before the user has left the field once, and never validate per keystroke |
| Cross-field rules (confirm password, end date after start date) | Check on submit |

4. **Native messages or custom messages.** Browser error bubbles cannot be styled and use the browser's language, which may not match the page. If the form shows its own messages, put `novalidate` on the `form`, set `aria-invalid="true"` on the field, and link the message with `aria-describedby`. The Constraint Validation API (`validity`, `checkValidity()`, `setCustomValidity()`) still works with `novalidate`.
5. **Error message wording.** Say what is wrong and how to fix it, in the field's own terms. For example: "Enter your email address" when it is empty, and "Enter an email address in the correct format, like name@example.com" when it is malformed. Never rely on colour alone, and never say only "Invalid input".
6. **The error summary.** A list of links at the top of the form, one per error, each moving focus to its field. Use the same wording as the message beside the field.

### Step 6: Buttons and submission

1. Every button has an explicit `type`. Inside a `form`, a button with no type submits it.
2. One primary submit button per form, with a label that says what happens: "Create account", "Pay £24.00".
3. Guard against double submission by ignoring repeat submits while a request is in flight. Keep the button enabled until then, because a disabled button cannot explain itself to a keyboard or screen reader user.

### Step 7: Plan, check dependants and apply in batches

1. Turn the field table into a change list: file and line, current markup, new markup, rule.
2. For each `name`, `id` and class touched, search the server code, tests and analytics configuration (commands in Templates). Any `name` change goes to the user under guardrail 2.
3. Group the changes into batches of at most 2 forms or 30 field changes.
4. Gate 2: show each batch's diff and wait for a yes before applying it.

### Step 8: Verify

1. **Checker.** Re-run `forms_check.py`. No errors in the changed forms and no new warnings.
2. **Autofill.** In a test browser profile with fake saved details, fill each form from the browser's suggestions. Every field fills with the right value. Sign-in offers the saved test password.
3. **Keyboard.** Tab order follows the visual order. Enter in a text field submits the form. Radios move with arrow keys inside their group.
4. **Errors.** Submit the form empty in the test environment. The summary appears, receives focus, its links move to the fields, the title starts with `Error: `, and every entry is kept.
5. **Screen reader.** Each field announces its label, hint and required state. Each error is announced with its field.
6. **Zoom.** At 200% zoom and a 320 CSS pixel wide window, nothing is cut off and labels stay beside their fields. (The nmor accessible-forms skill uses the same check.)
7. **Server.** Submit invalid data with client checks bypassed (for example, `novalidate` added in devtools) in the test environment. The server rejects it with the same messages.

### Step 9: Report

Use the report template. State fields changed, server gaps, names left for the user, and what was not checked.

## Worked example

The checkout form below is invented. The checker output is real: it was produced by running the script in this skill on these files.

**Input.** A guest checkout in plain HTML. The server validates every field. Analytics reads the `name` attributes. The team has error message and summary components.

```html
<form action="/checkout" method="post">
  <input type="text" name="fullname" placeholder="Full name">
  <label>Email <input type="text" name="email" autocomplete="off"></label>
  <label for="phone">Phone</label>
  <input type="number" id="phone" name="phone">
  <label for="pc">Postcode</label>
  <input id="pc" name="postcode" autocomplete="zip">
  <p>Delivery speed</p>
  <input type="radio" name="speed" value="std" id="std"><label for="std">Standard</label>
  <input type="radio" name="speed" value="exp" id="exp"><label for="exp">Express</label>
  <button class="apply-voucher">Apply voucher</button>
  <button>Pay now</button>
</form>
```

**Step 1, checker output** (tabs shown as spaces):

```
checkout_before.html:2   error  no-label                   input fullname (placeholder is not a label)
checkout_before.html:2   warn   missing-autocomplete       fullname: try name
checkout_before.html:3   warn   autocomplete-off-on-login  on a login-type field; browsers may ignore it and it blocks password managers
checkout_before.html:3   warn   type-mismatch              email: type=text, expected email
checkout_before.html:5   warn   missing-autocomplete       phone phone: try tel
checkout_before.html:5   warn   number-for-identifier      phone phone: type=number is for quantities
checkout_before.html:5   warn   type-mismatch              phone phone: type=number, expected tel
checkout_before.html:7   error  bad-autocomplete           "zip" is not a recognised token
checkout_before.html:9   error  group-without-fieldset     2 radio inputs named speed
checkout_before.html:11  warn   button-without-type        defaults to submit inside a form
checkout_before.html:12  warn   button-without-type        defaults to submit inside a form
checkout_before.html: 3 errors, 8 warnings
```

**Steps 2 to 6, decisions.**

| Field | Change | Rule |
|---|---|---|
| fullname | Visible label "Full name", `autocomplete="shipping name"` | Step 3.1, step 4 |
| email | `type="email"`, `autocomplete="email"`, `spellcheck="false"`, `off` removed | Step 4 table and rule 2 |
| phone | `type="tel"`, `autocomplete="tel"` | Step 4 table |
| postcode | `autocomplete="shipping postal-code"` in place of `zip` | Step 4 rule 5 |
| speed | `fieldset` with `legend` "Delivery speed" | Step 3.4 |
| Apply voucher | `type="button"` | Step 6.1 |
| Pay now | `type="submit"` | Step 6.1 |
| Form | `novalidate`, because the team shows its own messages | Step 5.4 |

No `name` changes, so analytics and the server are unaffected. The user approves the batch.

**After:**

```html
<form action="/checkout" method="post" novalidate>
  <label for="fullname">Full name</label>
  <input type="text" id="fullname" name="fullname" autocomplete="shipping name">
  <label for="email">Email address</label>
  <input type="email" id="email" name="email" autocomplete="email" spellcheck="false">
  <label for="phone">Phone number</label>
  <input type="tel" id="phone" name="phone" autocomplete="tel">
  <label for="pc">Postcode</label>
  <input type="text" id="pc" name="postcode" autocomplete="shipping postal-code">
  <fieldset>
    <legend>Delivery speed</legend>
    <input type="radio" name="speed" value="std" id="std"><label for="std">Standard</label>
    <input type="radio" name="speed" value="exp" id="exp"><label for="exp">Express</label>
  </fieldset>
  <button type="button" class="apply-voucher">Apply voucher</button>
  <button type="submit">Pay now</button>
</form>
```

**Step 8, checker output:** `checkout_after.html: 0 errors, 0 warnings`. The user runs the autofill, keyboard and error checks in the test environment and confirms them. The report notes one item for the user: the "Pay now" label could state the amount.

## Templates

### Checker

`scripts/forms_check.py` ships with this skill. Its autocomplete token list follows the WHATWG field names; **verify it against the current spec before relying on it**.

```
python3 scripts/forms_check.py form.html other.html
```

It exits with status 1 if any error is found, so it can run in CI.

### Field table

```
| Field | Purpose | Label | Hint | type | inputmode | autocomplete | Required | Client rule | Server rule | Error messages |
|---|---|---|---|---|---|---|---|---|---|---|
| email | Contact | Email address | We'll send your receipt here | email | | email | yes | format | format | Enter your email address / Enter an email address in the correct format, like name@example.com |
```

### Field with hint and error

```html
<div class="field field--error">
  <label for="email">Email address</label>
  <p id="email-hint" class="hint">We'll send your receipt here</p>
  <p id="email-error" class="error-message">
    <span class="visually-hidden">Error:</span> Enter an email address in the correct format, like name@example.com
  </p>
  <input type="email" id="email" name="email" autocomplete="email" spellcheck="false"
         aria-describedby="email-hint email-error" aria-invalid="true">
</div>
```

### Error summary

```html
<div class="error-summary" role="alert" tabindex="-1">
  <h2>There is a problem</h2>
  <ul>
    <li><a href="#email">Enter an email address in the correct format, like name@example.com</a></li>
    <li><a href="#pc">Enter your postcode</a></li>
  </ul>
</div>
```

Move focus to the summary when it appears, and prefix the page title: `<title>Error: Checkout</title>`.

### Finding dependants

```
grep -rnE "name=['\"]?(email|phone|postcode)" --include=*.py --include=*.rb --include=*.php --include=*.js --include=*.ts .
grep -rnE "(email|phone|postcode)" --include=*.test.* --include=*.spec.* .
```

Replace the field names with the ones being changed.

### Report

```
HTML FORMS REPORT: [forms], [date], branch [name]

Server validation: [confirmed for all fields / gaps: list]
Baseline: [n] errors, [n] warnings
Fixed: labels [n], autocomplete [n], types [n], grouping [n], buttons [n]
Timing model: [submit / existing blur pattern]
Names left for the user: [list, or none]
Fields to question (may not be needed): [list]
Verification: checker [0 errors], autofill [pass/fail], keyboard [pass/fail],
              error flow [pass/fail], screen reader [pass/fail], zoom [pass/fail], server [pass/fail]
Not checked: [custom widgets, live site, browsers not tested]
Legal wording (consent, privacy) routed to: [person]
```

### Starting prompt

```
Use the html-forms skill on [form files]. It is a [purpose] form built with
[stack]. The server validates in [path]. Start with the checker and the field
table, and do not change any name attributes or files until I approve.
```

## Failure modes and anti-patterns

| # | Failure | How to spot it | What to do instead |
|---|---|---|---|
| 1 | Placeholder used as the label | The checker flags `no-label` with a placeholder note; the field is blank-looking once typed in | A visible `label` above the field. Move format help into a linked hint |
| 2 | A `name` renamed during clean-up | Submissions arrive empty on the server, or analytics loses a field | Never rename `name` without the user's yes and a matching server change. Change `id` and labels freely |
| 3 | Fighting autofill | `autocomplete="off"` on sign-in fields, `readonly` hacks, random tokens on ordinary fields | Correct tokens on every personal field. Reserve the unrecognised-token approach for fields that must not take the user's own data |
| 4 | Client-only validation | Disabling scripts or adding `novalidate` lets bad data through | Server rules for every field, tested in step 8.7 |
| 5 | Validation that punishes | Errors appear while the user is still typing, messages say "Invalid", or only a red border marks the error | Submit-time validation with a summary and specific messages, or the existing blur pattern with no error before the first blur |

## Quality bar

Check every item before handing over.

1. The user approved gate 1, and the work is on a named branch.
2. The baseline checker output is saved and quoted in the report.
3. A field table exists with every cell filled or marked "none".
4. Every control has a visible label linked to it.
5. Every personal-data field has a recognised `autocomplete` token, and no sign-in field has `off`.
6. No identifier field (phone, postcode, card) uses `type="number"`.
7. Every radio or checkbox group with one name sits in a `fieldset` with a `legend`.
8. Every button has an explicit `type`.
9. The report states whether the server validates every field.
10. No `name` attribute changed without the user's recorded yes.
11. The checker shows zero errors on the changed forms.
12. The error flow test passed: summary, focus, title prefix and kept values.
13. The report and examples contain no real personal data, passwords, codes or card numbers.

## Cost and effort

The checker runs locally and makes no network calls. The agent reads its output and the form markup only, so token use stays small. The cheapest path for a large site is to fix the shared field and error components once, then re-run the checker on every page that uses them.

## Handoffs to related skills

`semantic-html.md` covers buttons, links, headings and landmarks outside forms. `form-optimisation.md` and `signup-flow-optimisation.md` cover cutting fields and raising completion rates. `checkout-optimisation.md` covers the wider checkout journey. `ai-security-privacy.md` covers what form data may go to AI tools.

## Appendix: tool-specific notes

Every fact below was read on 23 October 2026. **Verify before relying on this**: browsers and specs change.

### autocomplete (MDN)

- Values are `on`, `off` or an ordered list of tokens. Controls without the attribute inherit the value from their `form`.
- A leading `section-` token groups fields that share autofill data. `shipping` and `billing` separate duplicate fields.
- `off` often will not stop password managers from offering to save or fill credentials, and browsers may ignore it on login fields.
- Hidden inputs cannot use `on` or `off`.
- WCAG 2.2 SC 1.3.5 (Level AA) requires the purpose of user-information fields to be programmatically determinable. Valid tokens help meet it, and invalid tokens fail it.

### Constraint validation (MDN)

- Built-in attributes: `required`, `minlength`, `maxlength`, `min`, `max`, `step`, `type` and `pattern`.
- `:user-valid` and `:user-invalid` match only after the user has interacted with the field.
- `setCustomValidity("")` clears a custom error. Clear it on every `input` event, or the field stays invalid.
- `novalidate` turns off the browser's bubbles and keeps the API and pseudo-classes working.
- Native bubbles cannot be styled and use the browser's language.

### Autofill (web.dev)

- Browsers save values by `name`, and some also by `id`.
- `autocomplete="one-time-code"` is supported from Safari 14. On Chrome for Android, the WebOTP API covers SMS codes.
- Use native controls such as `select` for card expiry. Custom elements may not support autofill.
- `:autofill` styles autofilled fields.
- The page was last updated on 3 November 2021, so check current browser support.

### GOV.UK Design System validation pattern

Validate on submit, not when a field loses focus. Redisplay with entries kept, prefix the title with `Error: `, show an error summary with focus moved to it, and place a message beside each field. Always validate on the server. GOV.UK services turn off HTML5 validation with `novalidate` and do not use the `required` attribute. The team lists client-side validation as an open research question.

### Chrome and autocomplete="off"

Adam Silver (4 February 2021) reported that Chrome stopped filling a field when `name`, `id` and `autocomplete` were all set to a value it did not recognise, and that `new-password` on all three stopped autofill on create-password fields. A 2019 post by Leonardo Faria reported `off` and `new-password` failing for him, and a `readonly`-until-focus workaround. Both depend on browser versions. Test in your target browsers.

### Claude Agent Skills

Anthropic limits `name` to 64 characters and `description` to 1,024. Claude Code documents `disable-model-invocation: true` for skills with side effects, so they run only when the user calls them. Install skills only from sources you trust.

## Sources

Last verified 23 October 2026 for every source below, except where a date says otherwise.

- MDN, HTML attribute: autocomplete: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete.
- MDN, Client-side form validation: https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Form_validation.
- W3C WAI, Forms tutorial (updated 27 March 2026): https://www.w3.org/WAI/tutorials/forms/.
- WebAIM, The WebAIM Million (2026 report): https://webaim.org/projects/million/. 51% of home pages with missing form input labels.
- GOV.UK Design System, Recover from validation errors: https://design-system.service.gov.uk/patterns/validation.
- web.dev, Autofill (Learn Forms, last updated 3 November 2021): https://web.dev/learn/forms/autofill.
- web.dev, Sign-in form best practices codelab: https://web.dev/articles/codelab-sign-in-form-best-practices.
- Adam Silver, Stopping Chrome from ignoring autocomplete=off (4 February 2021): https://adamsilver.io/blog/stopping-chrome-from-ignoring-autocomplete-off/.
- Leonardo Faria, Disabling autofill in Chrome (2 October 2019): https://dev.to/leonardofaria/disabling-autofill-in-chrome-368k.
- Nmor, the-claude-council, accessible-forms skill (via Skills Directory): https://www.skillsdirectory.com/skills/nmor-accessible-forms. Rival implementation.
- agentsorg, benji, forms skill (via skills.sh): https://www.skills.sh/agentsorg/benji/forms. Rival implementation.
- joabgonzalez, ai-agents-skills, html skill (via skills.cat): https://skills.cat/skills/joabgonzalez/ai-agents-skills/html. Rival implementation.
- Anthropic, Agent Skills best practices (read 22 October 2026): https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices.
- Claude Code documentation, Skills (read 22 October 2026): https://code.claude.com/docs/en/skills.

---
Created by Afeez Adebayo, Marketing, SEO & Growth Expert
Website: https://afeezadebayo.com
LinkedIn: https://www.linkedin.com/in/afeez-adebayo/
X: https://x.com/za_icon
Source: https://afeezadebayo.com/skills-md/frontend-development/html-semantics/html-forms/

Licence: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/). You may copy, share and adapt this skill, including for commercial use, as long as you credit Afeez Adebayo and link to the source URL above. Keep this notice in any copy or adapted version.

Workflow

Step 0: Confirm scope and safety

  1. Confirm inputs 1 to 8 and the branch name.
  2. Tell the user the plan: checker, field table, decisions, batches, verification, report.
  3. Gate 1: wait for a yes.

Step 1: Run the checker

  1. Save scripts/forms_check.py from this skill folder. It uses only the Python standard library, reads files and writes nothing.
  2. Run it on rendered HTML where possible: python3 scripts/forms_check.py form.html.
  3. Keep the output as the baseline.
CodeLevelMeaning
no-labelerrorNo wrapping label, label for, aria-label or aria-labelledby. Notes when only a placeholder exists
bad-autocompleteerrorThe autocomplete value is not a recognised token
group-without-fieldseterrorTwo or more radios or checkboxes with one name sit outside a fieldset
fieldset-without-legenderrorA fieldset has no legend
missing-autocompletewarnThe field's name or id suggests personal data and it has no autocomplete
type-mismatchwarnAn email or phone field has the wrong type
number-for-identifierwarntype="number" on a phone, postcode or card field
autocomplete-off-on-loginwarnautocomplete="off" on a password, username or email field
no-namewarnThe control has no name, so it will not submit or autofill
button-without-typewarnA form has more than one button and some have no type
form-without-submitwarnNo submit button found

The checker reads names and IDs to guess a field's purpose. Its guesses are prompts for the field table, not decisions.

Step 2: Build the field table

Write one row per field, using the template in the Templates section: purpose, visible label, hint, type, inputmode, autocomplete, required or optional, client rule, server rule and error messages. Every later decision fills a cell in this table. A field with an empty server rule is a blocker under guardrail 4.

Step 3: Labels, hints and grouping

  1. Every control has a visible label that stays on screen while typing. A placeholder is not a label: it disappears as soon as the user types.
  2. Link the label with for and id, or wrap the control in the label.
  3. Put format help in a hint below the label, linked with aria-describedby. Do not hide the hint in a placeholder.
  4. Group related controls in fieldset with a legend: radio sets, checkbox sets, date parts, and address blocks that repeat (billing and shipping).
  5. Mark required or optional fields consistently. If most fields are required, mark the optional ones "(optional)". State the convention once at the top of the form. Do not rely on colour or an asterisk alone.
  6. Ask only for data the transaction needs. The W3C forms tutorial notes that extra fields raise abandonment. If a field looks unneeded, list it for the user to decide. The agent does not remove it.

Step 4: Types, keyboards and autofill

Use this table. Browsers save and offer values by name, and some also use id, so keep both meaningful.

PurposetypeautocompleteNotes
Full nametextnameUse given-name and family-name only if the server needs them split
EmailemailemailAdd spellcheck="false"
PhoneteltelKeep it in one input. Do not use type="number"
Username on sign-intext or emailusernameUse username even when the username is an email
Password on sign-inpasswordcurrent-passwordNever off
Password on sign-up or resetpasswordnew-passwordState the rules before submission and link them with aria-describedby
One-time codetextone-time-codeAdd inputmode="numeric"
Address linetextaddress-line1, address-line2Or street-address in a single textarea
Town or citytextaddress-level2
Postcodetextpostal-codeAccept spaces and lower case. Not type="number"
Countryselectcountry or country-name
Card numbertextcc-numberinputmode="numeric", one input
Card expirytext or selectcc-exp, or cc-exp-month and cc-exp-yearNative select if separate
Card security codetextcc-cscinputmode="numeric"
Quantity, age, amountnumbernonetype="number" is for values you count or calculate

Rules:

  1. Two addresses on one page. Prefix tokens with shipping or billing: shipping postal-code, billing postal-code.
  2. Never disable autofill on sign-in fields. MDN states that autocomplete="off" often will not stop password managers and browsers may ignore it on login fields. Turning it off only hurts the users who can still use it.
  3. Use off only for one-off values that are not the user's data, such as a CAPTCHA answer or a honeypot field.
  4. If a field must not take the user's own saved data (an admin entering another person's email), use a token the browser does not recognise for autocomplete and give name and id the same unrecognised value. Adam Silver reported in 2021 that this stopped Chrome filling the field. It depends on browser behaviour, so verify before relying on this in your target browsers.
  5. Never invent tokens. A made-up value such as zip defeats autofill and fails WCAG 2.2 success criterion 1.3.5, Identify Input Purpose.

Step 5: Validation

Apply these rules in order.

  1. The server validates every field. Always. Client checks are for speed and help only. Reuse the project's existing schema or validation library on both sides. A second library lets client and server rules drift apart (the benji forms skill makes the same point).
  2. Accept reasonable formats. Strip spaces from postcodes, card numbers and phone numbers. Accept upper or lower case. Ignore stray punctuation that does not change the meaning.
  3. Choose one timing model for the whole form.
SituationModel
DefaultValidate when the user submits. Redisplay the page with every entry kept, an error summary at the top with focus moved to it, Error: at the start of the page <title>, and a message beside each field. This is the GOV.UK Design System pattern
Feedback that adds information as the user typesLive feedback is fine: character counts, password rules met so far
The design system already validates when a field loses focusKeep it for consistency. Never show an error before the user has left the field once, and never validate per keystroke
Cross-field rules (confirm password, end date after start date)Check on submit
  1. Native messages or custom messages. Browser error bubbles cannot be styled and use the browser's language, which may not match the page. If the form shows its own messages, put novalidate on the form, set aria-invalid="true" on the field, and link the message with aria-describedby. The Constraint Validation API (validity, checkValidity(), setCustomValidity()) still works with novalidate.
  2. Error message wording. Say what is wrong and how to fix it, in the field's own terms. For example: "Enter your email address" when it is empty, and "Enter an email address in the correct format, like name@example.com" when it is malformed. Never rely on colour alone, and never say only "Invalid input".
  3. The error summary. A list of links at the top of the form, one per error, each moving focus to its field. Use the same wording as the message beside the field.

Step 6: Buttons and submission

  1. Every button has an explicit type. Inside a form, a button with no type submits it.
  2. One primary submit button per form, with a label that says what happens: "Create account", "Pay £24.00".
  3. Guard against double submission by ignoring repeat submits while a request is in flight. Keep the button enabled until then, because a disabled button cannot explain itself to a keyboard or screen reader user.

Step 7: Plan, check dependants and apply in batches

  1. Turn the field table into a change list: file and line, current markup, new markup, rule.
  2. For each name, id and class touched, search the server code, tests and analytics configuration (commands in Templates). Any name change goes to the user under guardrail 2.
  3. Group the changes into batches of at most 2 forms or 30 field changes.
  4. Gate 2: show each batch's diff and wait for a yes before applying it.

Step 8: Verify

  1. Checker. Re-run forms_check.py. No errors in the changed forms and no new warnings.
  2. Autofill. In a test browser profile with fake saved details, fill each form from the browser's suggestions. Every field fills with the right value. Sign-in offers the saved test password.
  3. Keyboard. Tab order follows the visual order. Enter in a text field submits the form. Radios move with arrow keys inside their group.
  4. Errors. Submit the form empty in the test environment. The summary appears, receives focus, its links move to the fields, the title starts with Error: , and every entry is kept.
  5. Screen reader. Each field announces its label, hint and required state. Each error is announced with its field.
  6. Zoom. At 200% zoom and a 320 CSS pixel wide window, nothing is cut off and labels stay beside their fields. (The nmor accessible-forms skill uses the same check.)
  7. Server. Submit invalid data with client checks bypassed (for example, novalidate added in devtools) in the test environment. The server rejects it with the same messages.

Step 9: Report

Use the report template. State fields changed, server gaps, names left for the user, and what was not checked.

Worked example

The checkout form below is invented. The checker output is real: it was produced by running the script in this skill on these files.

Input. A guest checkout in plain HTML. The server validates every field. Analytics reads the name attributes. The team has error message and summary components.

<form action="/checkout" method="post">
  <input type="text" name="fullname" placeholder="Full name">
  <label>Email <input type="text" name="email" autocomplete="off"></label>
  <label for="phone">Phone</label>
  <input type="number" id="phone" name="phone">
  <label for="pc">Postcode</label>
  <input id="pc" name="postcode" autocomplete="zip">
  <p>Delivery speed</p>
  <input type="radio" name="speed" value="std" id="std"><label for="std">Standard</label>
  <input type="radio" name="speed" value="exp" id="exp"><label for="exp">Express</label>
  <button class="apply-voucher">Apply voucher</button>
  <button>Pay now</button>
</form>

Step 1, checker output (tabs shown as spaces):

checkout_before.html:2   error  no-label                   input fullname (placeholder is not a label)
checkout_before.html:2   warn   missing-autocomplete       fullname: try name
checkout_before.html:3   warn   autocomplete-off-on-login  on a login-type field; browsers may ignore it and it blocks password managers
checkout_before.html:3   warn   type-mismatch              email: type=text, expected email
checkout_before.html:5   warn   missing-autocomplete       phone phone: try tel
checkout_before.html:5   warn   number-for-identifier      phone phone: type=number is for quantities
checkout_before.html:5   warn   type-mismatch              phone phone: type=number, expected tel
checkout_before.html:7   error  bad-autocomplete           "zip" is not a recognised token
checkout_before.html:9   error  group-without-fieldset     2 radio inputs named speed
checkout_before.html:11  warn   button-without-type        defaults to submit inside a form
checkout_before.html:12  warn   button-without-type        defaults to submit inside a form
checkout_before.html: 3 errors, 8 warnings

Steps 2 to 6, decisions.

FieldChangeRule
fullnameVisible label "Full name", autocomplete="shipping name"Step 3.1, step 4
emailtype="email", autocomplete="email", spellcheck="false", off removedStep 4 table and rule 2
phonetype="tel", autocomplete="tel"Step 4 table
postcodeautocomplete="shipping postal-code" in place of zipStep 4 rule 5
speedfieldset with legend "Delivery speed"Step 3.4
Apply vouchertype="button"Step 6.1
Pay nowtype="submit"Step 6.1
Formnovalidate, because the team shows its own messagesStep 5.4

No name changes, so analytics and the server are unaffected. The user approves the batch.

After:

<form action="/checkout" method="post" novalidate>
  <label for="fullname">Full name</label>
  <input type="text" id="fullname" name="fullname" autocomplete="shipping name">
  <label for="email">Email address</label>
  <input type="email" id="email" name="email" autocomplete="email" spellcheck="false">
  <label for="phone">Phone number</label>
  <input type="tel" id="phone" name="phone" autocomplete="tel">
  <label for="pc">Postcode</label>
  <input type="text" id="pc" name="postcode" autocomplete="shipping postal-code">
  <fieldset>
    <legend>Delivery speed</legend>
    <input type="radio" name="speed" value="std" id="std"><label for="std">Standard</label>
    <input type="radio" name="speed" value="exp" id="exp"><label for="exp">Express</label>
  </fieldset>
  <button type="button" class="apply-voucher">Apply voucher</button>
  <button type="submit">Pay now</button>
</form>

Step 8, checker output: checkout_after.html: 0 errors, 0 warnings. The user runs the autofill, keyboard and error checks in the test environment and confirms them. The report notes one item for the user: the "Pay now" label could state the amount.

Templates

Checker

scripts/forms_check.py ships with this skill. Its autocomplete token list follows the WHATWG field names; verify it against the current spec before relying on it.

python3 scripts/forms_check.py form.html other.html

It exits with status 1 if any error is found, so it can run in CI.

Field table

| Field | Purpose | Label | Hint | type | inputmode | autocomplete | Required | Client rule | Server rule | Error messages |
|---|---|---|---|---|---|---|---|---|---|---|
| email | Contact | Email address | We'll send your receipt here | email | | email | yes | format | format | Enter your email address / Enter an email address in the correct format, like name@example.com |

Field with hint and error

<div class="field field--error">
  <label for="email">Email address</label>
  <p id="email-hint" class="hint">We'll send your receipt here</p>
  <p id="email-error" class="error-message">
    <span class="visually-hidden">Error:</span> Enter an email address in the correct format, like name@example.com
  </p>
  <input type="email" id="email" name="email" autocomplete="email" spellcheck="false"
         aria-describedby="email-hint email-error" aria-invalid="true">
</div>

Error summary

<div class="error-summary" role="alert" tabindex="-1">
  <h2>There is a problem</h2>
  <ul>
    <li><a href="#email">Enter an email address in the correct format, like name@example.com</a></li>
    <li><a href="#pc">Enter your postcode</a></li>
  </ul>
</div>

Move focus to the summary when it appears, and prefix the page title: <title>Error: Checkout</title>.

Finding dependants

grep -rnE "name=['\"]?(email|phone|postcode)" --include=*.py --include=*.rb --include=*.php --include=*.js --include=*.ts .
grep -rnE "(email|phone|postcode)" --include=*.test.* --include=*.spec.* .

Replace the field names with the ones being changed.

Report

HTML FORMS REPORT: [forms], [date], branch [name]

Server validation: [confirmed for all fields / gaps: list]
Baseline: [n] errors, [n] warnings
Fixed: labels [n], autocomplete [n], types [n], grouping [n], buttons [n]
Timing model: [submit / existing blur pattern]
Names left for the user: [list, or none]
Fields to question (may not be needed): [list]
Verification: checker [0 errors], autofill [pass/fail], keyboard [pass/fail],
              error flow [pass/fail], screen reader [pass/fail], zoom [pass/fail], server [pass/fail]
Not checked: [custom widgets, live site, browsers not tested]
Legal wording (consent, privacy) routed to: [person]

Starting prompt

Use the html-forms skill on [form files]. It is a [purpose] form built with
[stack]. The server validates in [path]. Start with the checker and the field
table, and do not change any name attributes or files until I approve.

Failure modes and anti-patterns

#FailureHow to spot itWhat to do instead
1Placeholder used as the labelThe checker flags no-label with a placeholder note; the field is blank-looking once typed inA visible label above the field. Move format help into a linked hint
2A name renamed during clean-upSubmissions arrive empty on the server, or analytics loses a fieldNever rename name without the user's yes and a matching server change. Change id and labels freely
3Fighting autofillautocomplete="off" on sign-in fields, readonly hacks, random tokens on ordinary fieldsCorrect tokens on every personal field. Reserve the unrecognised-token approach for fields that must not take the user's own data
4Client-only validationDisabling scripts or adding novalidate lets bad data throughServer rules for every field, tested in step 8.7
5Validation that punishesErrors appear while the user is still typing, messages say "Invalid", or only a red border marks the errorSubmit-time validation with a summary and specific messages, or the existing blur pattern with no error before the first blur

Quality bar

Check every item before handing over.

  1. The user approved gate 1, and the work is on a named branch.
  2. The baseline checker output is saved and quoted in the report.
  3. A field table exists with every cell filled or marked "none".
  4. Every control has a visible label linked to it.
  5. Every personal-data field has a recognised autocomplete token, and no sign-in field has off.
  6. No identifier field (phone, postcode, card) uses type="number".
  7. Every radio or checkbox group with one name sits in a fieldset with a legend.
  8. Every button has an explicit type.
  9. The report states whether the server validates every field.
  10. No name attribute changed without the user's recorded yes.
  11. The checker shows zero errors on the changed forms.
  12. The error flow test passed: summary, focus, title prefix and kept values.
  13. The report and examples contain no real personal data, passwords, codes or card numbers.

Cost and effort

The checker runs locally and makes no network calls. The agent reads its output and the form markup only, so token use stays small. The cheapest path for a large site is to fix the shared field and error components once, then re-run the checker on every page that uses them.

semantic-html.md covers buttons, links, headings and landmarks outside forms. form-optimisation.md and signup-flow-optimisation.md cover cutting fields and raising completion rates. checkout-optimisation.md covers the wider checkout journey. ai-security-privacy.md covers what form data may go to AI tools.

Appendix: tool-specific notes

Every fact below was read on 23 October 2026. Verify before relying on this: browsers and specs change.

autocomplete (MDN)

  • Values are on, off or an ordered list of tokens. Controls without the attribute inherit the value from their form.
  • A leading section- token groups fields that share autofill data. shipping and billing separate duplicate fields.
  • off often will not stop password managers from offering to save or fill credentials, and browsers may ignore it on login fields.
  • Hidden inputs cannot use on or off.
  • WCAG 2.2 SC 1.3.5 (Level AA) requires the purpose of user-information fields to be programmatically determinable. Valid tokens help meet it, and invalid tokens fail it.

Constraint validation (MDN)

  • Built-in attributes: required, minlength, maxlength, min, max, step, type and pattern.
  • :user-valid and :user-invalid match only after the user has interacted with the field.
  • setCustomValidity("") clears a custom error. Clear it on every input event, or the field stays invalid.
  • novalidate turns off the browser's bubbles and keeps the API and pseudo-classes working.
  • Native bubbles cannot be styled and use the browser's language.

Autofill (web.dev)

  • Browsers save values by name, and some also by id.
  • autocomplete="one-time-code" is supported from Safari 14. On Chrome for Android, the WebOTP API covers SMS codes.
  • Use native controls such as select for card expiry. Custom elements may not support autofill.
  • :autofill styles autofilled fields.
  • The page was last updated on 3 November 2021, so check current browser support.

GOV.UK Design System validation pattern

Validate on submit, not when a field loses focus. Redisplay with entries kept, prefix the title with Error: , show an error summary with focus moved to it, and place a message beside each field. Always validate on the server. GOV.UK services turn off HTML5 validation with novalidate and do not use the required attribute. The team lists client-side validation as an open research question.

Chrome and autocomplete="off"

Adam Silver (4 February 2021) reported that Chrome stopped filling a field when name, id and autocomplete were all set to a value it did not recognise, and that new-password on all three stopped autofill on create-password fields. A 2019 post by Leonardo Faria reported off and new-password failing for him, and a readonly-until-focus workaround. Both depend on browser versions. Test in your target browsers.

Claude Agent Skills

Anthropic limits name to 64 characters and description to 1,024. Claude Code documents disable-model-invocation: true for skills with side effects, so they run only when the user calls them. Install skills only from sources you trust.

Sources

Last verified 23 October 2026 for every source below, except where a date says otherwise.

What this skill does

Forms lose people in small ways: a placeholder that vanishes, a phone number in a number box, an error shown only as a red border. The WebAIM Million 2026 report found missing form input labels on 51% of one million home pages.

This skill runs a short Python checker that reports 11 problem codes with line numbers, then writes one row per field: purpose, label, hint, type, autocomplete token, required state, client rule, server rule and error messages. It applies rules from a table of field types and tokens, chooses one validation timing model for the whole form, and writes error messages that say how to fix the problem. Server-side validation is required on every field.

I read three published form skills before writing this one. The strongest covers labels and errors well but validates on blur without weighing the GOV.UK evidence for validating on submit. Another defers testing to a separate skill, and the third covers forms in two lines. None of them warns against renaming field names during a clean-up, which is the change most likely to break submissions.

Who it is for

Use it if you own a sign-in, sign-up, checkout, address or contact form and want it to work with autofill, password managers, keyboards and screen readers. It suits frontend developers, product teams reviewing AI-generated forms and anyone fixing a form after a drop in completions. It works with any form library, because the rules sit in the HTML and the field table, and it reuses your existing schema or validation code instead of adding a second one.

It is the wrong skill for buttons, headings and landmarks elsewhere on the page (use Semantic HTML), for cutting fields to raise conversion (use Form Optimisation), or for authentication security and legal consent wording.

What is inside

The file is 4,611 words (version 1.0.0), with a checker script in scripts/, and holds:

  • Nine guardrails, including no name changes without approval, no real form submissions, server validation required and no real personal data in tests.
  • Eight inputs the agent must have first, with the exact question for each.
  • A nine-step workflow with a field type and autocomplete table, a validation timing table and error message rules.
  • A standard-library Python checker in scripts/forms_check.py with the WHATWG autofill token list.
  • A worked example with an invented checkout form, run through the checker before and after.
  • Templates for a field table, a field with hint and error, an error summary, dependant searches and a report.
  • Five failure modes, a 13-item quality bar and a fenced appendix on MDN, web.dev, GOV.UK and Chrome autofill behaviour.

How to install and use it in Claude, Cursor and other agents

Claude. Keep the folder as html-forms/ with SKILL.md and scripts/forms_check.py inside, and zip it. In Claude, open Customize, then Skills, then Upload a skill. In Claude Code, put the folder in ~/.claude/skills/ or .claude/skills/ in a project.

Cursor and other agents. Put the folder where your tool reads skills, or paste the body into its rules and keep the script beside your code. Then send the starting prompt from the Templates section.

Before you install. Read the checker first. It reads files and writes nothing. The skill tests only in a test environment with fake data. Install skills only from sources you trust.

A worked example

An invented guest checkout has a name field labelled only by its placeholder, an email field with type text and autocomplete off, a phone number in a number input, the invented token zip on the postcode, delivery radios with no fieldset and two buttons with no type. The checker reports 3 errors and 8 warnings.

The agent adds visible labels, sets email, tel and shipping postal-code tokens, groups the radios under a legend, gives each button an explicit type and adds novalidate because the team shows its own messages. No name attribute changes, so the server and analytics are unaffected. After one approved batch, the checker reports 0 errors and 0 warnings.

Common mistakes

The skill is built around five ways this goes wrong:

  • Placeholder as label. It disappears as soon as the user types. The skill adds a visible label and moves format help into a linked hint.
  • Renaming name attributes. Submissions and analytics break. The skill changes ids and labels freely but never a name without your yes.
  • Fighting autofill. Browsers may ignore autocomplete off on login fields, so the skill sets correct tokens instead.
  • Client-only validation. The server checks every field, and the skill tests that with client checks bypassed.
  • Validation that punishes. Errors while the user is still typing, or messages that only say invalid. The skill validates on submit with a summary by default.

Semantic HTML covers markup outside forms. Form Optimisation, Sign-up Flow Optimisation and Checkout Optimisation cover completion rates. Browse the HTML and Semantics set.

Frequently asked questions

No. A placeholder disappears as soon as the user types, so people lose the question halfway through, and it is not a reliable accessible name. Every field needs a visible label linked with for and id or by wrapping. This skill flags placeholder-only fields as errors and moves any format help into a hint linked with aria-describedby.

MDN notes that browsers may ignore autocomplete off, especially on login fields, and that it often will not stop password managers. Blocking autofill on sign-in mainly hurts users. This skill sets the correct token, such as current-password or email, and keeps an unrecognised-token approach only for fields that must not take the user’s own data, tested in your target browsers.

Use postal-code, with shipping or billing in front when a page asks for two addresses, for example shipping postal-code. Invented values such as zip defeat autofill and fail WCAG 2.2 success criterion 1.3.5. Keep the input as type text, not number, so spaces, letters and leading zeros are accepted. The checker flags both problems.

The GOV.UK Design System validates on submit, redisplays the page with entries kept, adds an error summary with focus and prefixes the page title with Error. This skill uses that as its default. If your design system already validates when a field loses focus, it keeps that for consistency but never shows an error before the user has left the field once.

No. Client-side checks are easy to bypass, so the server must validate every field. Native browser bubbles also cannot be styled and appear in the browser’s language. This skill requires server rules in the field table, uses novalidate when the team shows its own messages, and tests the server with client checks switched off in a test environment.

No. Number inputs are for quantities you count or calculate. Phone numbers, postcodes and card numbers are identifiers that can have spaces, plus signs or leading zeros. Use type tel with autocomplete tel for phones, and type text with inputmode numeric for card numbers. The checker flags number inputs on identifier fields.

Changelog

  • 1.0.0, 23 October 2026: first release.

Use, adapt and share this skill under CC BY 4.0: credit Afeez Adebayo and link to this page.