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.
By Afeez Adebayo, Marketing, SEO & Growth Expert with 10+ years of experience.
Created 23 October 2026. Updated 23 October 2026. Free to use under CC BY 4.0.
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,inputmodeandautocompletefor 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
- 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.
- Never rename
nameattributes 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. - 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.
- 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.
- 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. - Batches stay small. At most 2 forms or 30 field changes per batch (defaults), each shown as a diff.
- 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.
- 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.
- 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.
Raw SKILL.md file
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
- Confirm inputs 1 to 8 and the branch name.
- Tell the user the plan: checker, field table, decisions, batches, verification, report.
- Gate 1: wait for a yes.
Step 1: Run the checker
- Save
scripts/forms_check.pyfrom this skill folder. It uses only the Python standard library, reads files and writes nothing. - Run it on rendered HTML where possible:
python3 scripts/forms_check.py form.html. - 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
- 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.
- Link the label with
forandid, or wrap the control in thelabel. - Put format help in a hint below the label, linked with
aria-describedby. Do not hide the hint in a placeholder. - Group related controls in
fieldsetwith alegend: radio sets, checkbox sets, date parts, and address blocks that repeat (billing and shipping). - 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.
- 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 | 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:
- Two addresses on one page. Prefix tokens with
shippingorbilling:shipping postal-code,billing postal-code. - 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. - Use
offonly for one-off values that are not the user's data, such as a CAPTCHA answer or a honeypot field. - 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
autocompleteand givenameandidthe 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. - Never invent tokens. A made-up value such as
zipdefeats autofill and fails WCAG 2.2 success criterion 1.3.5, Identify Input Purpose.
Step 5: Validation
Apply these rules in order.
- 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).
- 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.
- 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 |
- 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
novalidateon theform, setaria-invalid="true"on the field, and link the message witharia-describedby. The Constraint Validation API (validity,checkValidity(),setCustomValidity()) still works withnovalidate. - 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".
- 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
- Every button has an explicit
type. Inside aform, a button with no type submits it. - One primary submit button per form, with a label that says what happens: "Create account", "Pay £24.00".
- 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
- Turn the field table into a change list: file and line, current markup, new markup, rule.
- For each
name,idand class touched, search the server code, tests and analytics configuration (commands in Templates). Anynamechange goes to the user under guardrail 2. - Group the changes into batches of at most 2 forms or 30 field changes.
- Gate 2: show each batch's diff and wait for a yes before applying it.
Step 8: Verify
- Checker. Re-run
forms_check.py. No errors in the changed forms and no new warnings. - 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.
- Keyboard. Tab order follows the visual order. Enter in a text field submits the form. Radios move with arrow keys inside their group.
- 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. - Screen reader. Each field announces its label, hint and required state. Each error is announced with its field.
- 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.)
- Server. Submit invalid data with client checks bypassed (for example,
novalidateadded 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 warningsSteps 2 to 6, decisions.
| Field | Change | Rule |
|---|---|---|
| fullname | Visible label "Full name", autocomplete="shipping name" | Step 3.1, step 4 |
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:
<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.htmlIt 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
| # | 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.
- The user approved gate 1, and the work is on a named branch.
- The baseline checker output is saved and quoted in the report.
- A field table exists with every cell filled or marked "none".
- Every control has a visible label linked to it.
- Every personal-data field has a recognised
autocompletetoken, and no sign-in field hasoff. - No identifier field (phone, postcode, card) uses
type="number". - Every radio or checkbox group with one name sits in a
fieldsetwith alegend. - Every button has an explicit
type. - The report states whether the server validates every field.
- No
nameattribute changed without the user's recorded yes. - The checker shows zero errors on the changed forms.
- The error flow test passed: summary, focus, title prefix and kept values.
- 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,offor an ordered list of tokens. Controls without the attribute inherit the value from theirform. - A leading
section-token groups fields that share autofill data.shippingandbillingseparate duplicate fields. offoften will not stop password managers from offering to save or fill credentials, and browsers may ignore it on login fields.- Hidden inputs cannot use
onoroff. - 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,typeandpattern. :user-validand:user-invalidmatch only after the user has interacted with the field.setCustomValidity("")clears a custom error. Clear it on everyinputevent, or the field stays invalid.novalidateturns 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 byid. autocomplete="one-time-code"is supported from Safari 14. On Chrome for Android, the WebOTP API covers SMS codes.- Use native controls such as
selectfor card expiry. Custom elements may not support autofill. :autofillstyles 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.
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.pywith 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.
Related skills
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
Is a placeholder enough instead of a label?
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.
Why does Chrome ignore autocomplete off?
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.
What autocomplete value should a postcode field use?
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.
Should forms validate on blur or on submit?
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.
Is HTML5 form validation enough?
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.
Should a phone number input use type number?
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.
All HTML and Semantics skills
