Test form label associations
Form inputs must have labels that are programmatically associated. Visual labels or instructions must be provided when content requires user input.
Form with labels demo
What users hear
Name field:
"edit text" (no label!)
Email field:
"edit text" (no context!)
Checkbox:
"checkbox, not checked" (what for?)
Radio group:
"radio button" (which option?)
Different approaches
<label for="email">Email</label> <input id="email" type="email" /> <!-- Benefits: --> <!-- Clicking label focuses input --> <!-- Clear association --> <!-- Widest support -->
<label> Email <input type="email" /> </label> <!-- Benefits: --> <!-- No need for id attribute --> <!-- Cleaner markup --> <!-- Same accessibility -->
<input type="search" aria-label="Search products" /> <!-- Use when: --> <!-- Visual label not needed --> <!-- Icon-only buttons --> <!-- Space constraints -->
<label for="pass">Password</label> <input id="pass" type="password" aria-describedby="pass-hint" /> <p id="pass-hint"> Must be 8+ characters </p>
Testing hints
import AxeBuilder from '@axe-core/playwright';
// Check for missing labels
const results = await new AxeBuilder({ page })
.withRules(['label', 'label-title-only'])
.analyze();
// Find inputs without labels
const inputs = await page.$$('input, select, textarea');
for (const input of inputs) {
const id = await input.getAttribute('id');
const ariaLabel = await input.getAttribute('aria-label');
const hasLabel = id
? await page.$(`label[for="${id}"]`)
: await input.evaluate(
el => el.closest('label') !== null
);
if (!hasLabel && !ariaLabel) {
console.log('Missing label');
}
}// Playwright's getByLabel uses accessible names
const emailInput = page.getByLabel('Email Address');
await expect(emailInput).toBeVisible();
// Check required fields have aria-required
const required = await page.$$('[aria-required="true"]');
expect(required.length).toBeGreaterThan(0);
// Verify fieldset/legend for radio groups
const fieldset = page.locator('fieldset');
const legend = fieldset.locator('legend');
await expect(legend).toHaveText('Gender');
// Check aria-describedby links exist
const input = page.getByLabel('Email');
const describedBy = await input.getAttribute(
'aria-describedby'
);
if (describedBy) {
const hint = page.locator(`#${describedBy}`);
await expect(hint).toBeAttached();
}Automation hints
page.getByLabel('Email Address')page.locator('label[for="email-input"]')getAttribute('aria-required')page.locator('fieldset legend')