Find and fix accessibility problems in HTML
Check a piece of HTML against WCAG 2.2 AA, rank the problems by who they block, and get the corrected code.
by Appndo
Tested on GPT-6.1 Sol, Gemini 3.8 Flash · Verified Nov 2, 2026
The prompt
Review this HTML for accessibility problems.
Code (HTML, plus CSS or JavaScript where it matters):
{{code}}
What the component does, and what the code doesn't show (colours, behaviour):
{{context}}
1. List each problem with the element, what goes wrong and for whom (screen reader users, keyboard users, people with low vision, voice control users), and the WCAG 2.2 success criterion it fails, by number and name.
2. Order the list: first problems that stop someone finishing the task, then ones that make it harder, then minor ones.
3. For each problem give the corrected code. Prefer native HTML elements to ARIA; use ARIA only where no native element does the job.
4. Where colours are given, work out the contrast ratio and compare it with the required level. Say which checks still need a real browser or a person (focus order across the page, what a screen reader announces) and how to do them.
5. Finish with the full corrected snippet.
Report only problems you can see in the code or the description, and don't add requirements it doesn't state (for example making a field required). If something depends on code I didn't include, say what you'd need to see instead of assuming it's broken.How to use it
Paste one component or section at a time, with the CSS or script that changes how it behaves. Describe the colours and interactions the code doesn't show. Try the fixed version with only a keyboard and with a screen reader (VoiceOver or NVDA) before you ship it.
Example
Inputs
- code
- <form class="signup"> <h3>Create your account</h3> <input type="text" placeholder="Email"> <input type="password" placeholder="Password"> <span class="hint">8+ characters</span> <div class="error" style="color:#e00">Password too short</div> <label><input type="checkbox"> I agree to the <a href="/terms">terms</a></label> <div class="btn" onclick="submitForm()">Sign up</div> <button type="button" class="show-pw"><svg viewBox="0 0 24 24"><path d="M12 5c-7 0-10 7-10 7s3 7 10 7 10-7 10-7-3-7-10-7z"/></svg></button> </form>
- context
- Sign-up form on our landing page. Placeholder text is light grey #aaaaaa on a white background. The error line appears under the password field after a failed submit; the fields themselves don't change. The eye button shows or hides the password. The h3 comes straight after the page's h1.
Output
The clearest blockers are the mouse-only Sign up control and the unnamed password visibility button. Other problems affect field identification, instructions, and placeholder contrast.
Problems, in priority order
1. Sign up is a clickable <div>
Element: <div class="btn" onclick="submitForm()">
A plain <div> is not keyboard focusable or keyboard activatable. Keyboard users cannot reach and activate this control. Screen readers do not identify it as a button, and voice control users cannot reliably target it as a button.
Fails:
- 2.1.1 Keyboard
- 4.1.2 Name, Role, Value — the button role is missing.
Correction:
<button type="submit" class="btn">Sign up</button>
Handle submission on the form’s submit event. This also supports native form submission from the keyboard. Whether another submission route already works depends on the omitted JavaScript; it does not repair this control.
2. The password visibility button has no accessible name
Element: <button class="show-pw"> containing only an SVG.
The SVG has no text alternative, so the button has no accessible name. Screen reader users cannot identify its purpose; voice control users have no meaningful name to target. This can prevent someone who needs to inspect their password from doing so confidently.
Fails: 4.1.2 Name, Role, Value.
Correction: Give the native button visible text that describes its current action, and update that text when the password visibility changes:
<button type="button" class="show-pw">Show password</button>
Change the text to Hide password when the password is visible. A changing action label does not also need aria-pressed. If you retain the decorative SVG alongside the text, give it aria-hidden="true".
The omitted toggle handler is needed to verify that it actually changes the input type and keeps the label accurate.
3. Email and password have only placeholders as labels
Elements: The text and password inputs.
Placeholders disappear when someone types. People with low vision or cognitive difficulties can lose track of a field’s purpose. Browsers may expose placeholders as fallback accessible names, but that does not provide persistent labels or a reliable visible target for voice control.
Fails: 3.3.2 Labels or Instructions.
Correction: Use explicitly associated, visible labels:
<label for="signup-email">Email</label>
<input id="signup-email" name="email" type="text"
inputmode="email" autocomplete="email">
<label for="signup-password">Password</label>
<input id="signup-password" name="password" type="password"
autocomplete="new-password">
These changes do not make either field required. The example retains type="text" for email to avoid introducing an additional format-validation constraint.
4. The password instruction and error are not associated with the input
Elements: .hint, .error, and the password input.
Their position communicates a relationship visually, but the input has no programmatic connection to either message. Screen reader users navigating directly between fields may miss the password rule or the explanation of the failed attempt.
Fails: 1.3.1 Info and Relationships.
Correction: Connect the instruction and, when present, the error using aria-describedby:
<label for="signup-password">Password</label>
<input id="signup-password" name="password" type="password"
autocomplete="new-password"
aria-describedby="password-hint password-error"
aria-invalid="true">
<span id="password-hint">8+ characters</span>
<div id="password-error">Password too short</div>
This example represents the failed-validation state. Before an error exists, omit aria-invalid and reference only password-hint.
aria-invalid explicitly exposes the invalid state; its absence alone is not automatically a separate failure. The existing error text already names the password and describes the problem, so there is no demonstrated 3.3.1 Error Identification failure merely because the field does not change colour.
5. Placeholder text has insufficient contrast
Elements: The placeholders, #aaaaaa on white.
The contrast ratio is approximately 2.32:1. This fails both the 4.5:1 requirement for ordinary text and the 3:1 requirement for large text. People with low vision may struggle to read it.
Fails: 1.4.3 Contrast (Minimum).
Correction: Remove the redundant placeholders after adding visible labels. If placeholders are retained, a colour such as #767676 on white provides approximately 4.54:1:
.signup input::placeholder {
color: #767676;
opacity: 1;
}
The opacity matters because browser placeholder styling can otherwise change the rendered colour.
Observations that are not confirmed failures
- Error colour:
#e00(#ee0000) on white has approximately 4.53:1 contrast. It passes 1.4.3 Contrast (Minimum) for ordinary text, narrowly. This assumes the error’s actual background is white. The written error also means colour is not the only way the problem is communicated; no 1.4.1 Use of Color failure is demonstrated. - Error announcement: Showing an error visually does not establish whether assistive technology announces it. If focus stays in place and the new error is not exposed as a status message, this can fail 4.1.3 Status Messages. Inspect the submit handler and test it. A pre-existing empty
role="alert"container, populated when validation fails, is one suitable approach. - Heading: An
h3immediately after anh1skips a level. That alone is not an automatic WCAG failure. If this form is a direct subsection of the page, useh2; an actual mismatch between the conveyed structure and heading levels can fail 1.3.1 Info and Relationships. - Checkbox: It already has a native, wrapping label. The link inside it is not, by itself, a demonstrated WCAG failure.
- Autocomplete: The corrected fields include
autocomplete="email"andautocomplete="new-password". Whether their omission fails 1.3.5 Identify Input Purpose depends on that criterion’s applicability, including the purpose of the password field; creating a new password is not the same as entering a current password.
Checks requiring a browser or a person
- Keyboard and page focus order: Use Tab and Shift+Tab through the entire landing page. Confirm the order preserves meaning and operation (2.4.3 Focus Order), every interactive control works without a mouse (2.1.1), and focus is visible and not entirely obscured (2.4.7 Focus Visible, 2.4.11 Focus Not Obscured (Minimum)). The snippet does not show the surrounding page or focus CSS.
- Screen reader output: Test with a browser/screen reader combination such as Safari with VoiceOver or Firefox with NVDA. Navigate by form controls and headings. Confirm labels, password instructions, invalid state, checkbox label, button names, and heading structure. Trigger a failed submission and confirm the error is announced without having to search for it.
- Password toggle: Activate it with Enter and Space. Confirm focus remains on the button, the password visibility changes, and the button’s accessible name changes to match its next action.
- Rendered styling: Inspect computed colours and backgrounds, including placeholder opacity. Check focus indicators, target sizes, zoom, and surrounding layout in the actual page; the supplied code cannot establish their compliance.
Full corrected snippet
This shows the initial state. The existing submission handler should call setPasswordError('Password too short') when its validation rejects the password, and setPasswordError('') when that error clears. Its validation and submission logic were not supplied, so they are not invented here. No field is made required.
<form class="signup">
<h2>Create your account</h2>
<label for="signup-email">Email</label>
<input
id="signup-email"
name="email"
type="text"
inputmode="email"
autocomplete="email"
>
<label for="signup-password">Password</label>
<input
id="signup-password"
name="password"
type="password"
autocomplete="new-password"
aria-describedby="password-hint"
>
<span id="password-hint">8+ characters</span>
<!-- Keep this empty container present before an error occurs. -->
<div id="password-error" class="error" role="alert"></div>
<button type="button" class="show-pw">Show password</button>
<label>
<input type="checkbox" name="terms">
I agree to the <a href="/terms">terms</a>
</label>
<button type="submit" class="btn">Sign up</button>
</form>
<style>
.signup {
color: #222;
background: #fff;
}
.signup .error {
color: #e00;
}
</style>
<script>
const form = document.querySelector('.signup');
const password = form.querySelector('#signup-password');
const error = form.querySelector('#password-error');
const toggle = form.querySelector('.show-pw');
toggle.addEventListener('click', () => {
const show = password.type === 'password';
password.type = show ? 'text' : 'password';
toggle.textContent = show ? 'Hide password' : 'Show password';
});
function setPasswordError(message) {
if (message) {
password.setAttribute('aria-invalid', 'true');
password.setAttribute(
'aria-describedby',
'password-hint password-error'
);
} else {
password.removeAttribute('aria-invalid');
password.setAttribute('aria-describedby', 'password-hint');
}
error.textContent = message;
}
form.addEventListener('submit', (event) => {
event.preventDefault();
// Existing application function: keep validation and submission here.
// On validation failure, it should call setPasswordError(message).
submitForm();
});
</script>
One real run with the example above. Results vary between runs and models.