Forms and Labels
Locate inputs the way users find them: by their label. Covers for/id association, wrapping labels, aria-labelledby, placeholders, selects, radios and checkboxes.
Forms and Labels
Users find a field by reading its label. A locator that does the same thing stays correct when ids change, when fields are reordered, and when the design system is swapped out. This lesson covers the three ways labels attach to inputs in HTML and the XPath for each.
Pattern 1: label for=“id”
<label for="email">Email address</label>
<input id="email" name="email" type="email">
The input’s id equals the label’s for. Resolve it with a node-set comparison:
//input[@id=//label[normalize-space()='Email address']/@for]
This is valid XPath 1.0: @id = (node-set) is true if the id equals any node in the set. It works regardless of where the label sits in the document.
If the label and input are siblings, the shorter form is fine:
//label[normalize-space()='Email address']/following-sibling::input[1]
Pattern 2: wrapping label
<label>
Remember me
<input type="checkbox" name="remember">
</label>
The input is a descendant of the label:
//label[contains(., 'Remember me')]//input
//label[contains(normalize-space(), 'Remember me')]/input[@type='checkbox']
Use contains(., ...) or normalize-space() here, because the label’s own text node has whitespace around it and the checkbox contributes nothing to the string value.
Pattern 3: aria-labelledby and aria-label
<span id="pw-label">Password</span>
<input aria-labelledby="pw-label" type="password">
<input aria-label="Search" type="search">
//input[@aria-labelledby=//*[normalize-space()='Password']/@id]
//input[@aria-label='Search']
aria-labelledby can contain several ids separated by spaces. If so, use contains(concat(' ', @aria-labelledby, ' '), ' pw-label ').
Placeholder as a last resort
//input[@placeholder='Enter your email']
Placeholders are copy, and copy changes. Rate this medium and prefer a label or name when either exists.
Selects, radios, checkboxes
Select and its options
//select[@name='country']
//select[@name='country']/option[normalize-space()='Germany']
//select[@name='country']/option[@value='DE']
//select[@name='country']/option[@selected]
Selenium’s Select helper and Playwright’s selectOption want the select element, not the option, so locate the select and let the framework choose.
Radio buttons by label
<input type="radio" id="plan-pro" name="plan" value="pro">
<label for="plan-pro">Pro</label>
//input[@type='radio'][@id=//label[normalize-space()='Pro']/@for]
//input[@type='radio'][@name='plan'][@value='pro']
The second is more direct when the value is meaningful.
Checkbox state
//input[@type='checkbox'][@checked] checked when the page loaded
XPath sees the checked attribute, which reflects the initial state, not the live property after a user clicks. Use your framework’s isSelected() or isChecked() for live state, and XPath only to find the element.
Form containers
Anchor to the form to avoid matching a same-named input in another form on the page:
//form[@data-testid='signup']//input[@name='email']
//form[.//h2[normalize-space()='Billing']]//input[@name='postcode']
//form[@action='/login']//button[@type='submit']
Validation messages
Error messages are usually siblings of the input or linked with aria-describedby:
//input[@name='email']/following-sibling::*[contains(@class, 'error')]
//*[@id=//input[@name='email']/@aria-describedby]
//input[@name='email'][@aria-invalid='true']
Buttons in forms
//form[@id='login']//button[@type='submit']
//button[@type='submit'][normalize-space()='Create account']
//input[@type='submit'][@value='Search'] old-style submit inputs
Complete example
<form data-testid="checkout">
<div class="field">
<label for="ck-1">Card number</label>
<input id="ck-1" name="cardNumber">
</div>
<div class="field">
<label for="ck-2">Expiry</label>
<input id="ck-2" name="expiry">
<span class="error" id="ck-2-err">Expired</span>
</div>
<button type="submit">Pay now</button>
</form>
//form[@data-testid='checkout']//input[@id=//label[normalize-space()='Card number']/@for]
//form[@data-testid='checkout']//input[@name='expiry']/following-sibling::span[@class='error']
//form[@data-testid='checkout']//button[normalize-space()='Pay now']
Try It Yourself
Open in Playground →
Next Steps
- Anchoring to Stable Context - Containers and landmarks
- XPath Axes Navigation -
following-sibling,preceding-siblingand friends - Challenges - Practise label-based locators against real forms