Every form control needs a programmatic label — not just a visual one. Screen readers, voice control, and form-fill tools all use the label to know what each field is for. Forms with unlabelled fields announce as "edit text, blank" — useless. This guide walks through every label pattern: visible labels, hidden labels, grouped labels for radios, and the placeholder-isn't-a-label trap. For related fixes, see the Accessibility Fixes index.
<label for="email">Email address</label> <input id="email" type="email" name="email">
This pattern provides three things at once: visible label text, programmatic association via for/id, clickable label that focuses the input.
<label> Email address <input type="email" name="email"> </label>
Equally valid. Choice between patterns is stylistic — wrapped is fine for simple forms, for/id is more flexible when label and input are visually separated.
Sometimes the design genuinely doesn't have room for a visible label — a search box in the nav, a single-field newsletter signup, an inline filter input. Three valid patterns:
<input type="search"
placeholder="Search products"
aria-label="Search products">
Screen reader announces "Search products, edit text". Sighted users see the placeholder. Less ideal than a real label but acceptable when design demands.
<h2 id="search-heading">Find what you need</h2>
<input type="search"
aria-labelledby="search-heading">
The input borrows the heading as its label. Useful when a nearby heading already describes the field's purpose.
<label for="search" class="sr-only">Search products</label>
<input id="search" type="search" placeholder="Search...">
<style>
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
white-space: nowrap;
border: 0;
}
</style>
Best of both worlds: a real label element (still clickable, programmatically associated) that's hidden visually.
Radio buttons and related checkboxes need a group label in addition to per-control labels. Fieldset and legend provide it.
<fieldset>
<legend>Shipping method</legend>
<label>
<input type="radio" name="shipping" value="standard">
Standard (3-5 days)
</label>
<label>
<input type="radio" name="shipping" value="express">
Express (next day)
</label>
</fieldset>
Screen reader announcement: "Shipping method, group. Standard, 3-5 days, radio button, 1 of 2."
<!-- WRONG --> <div class="label">Email</div> <input type="email"> <!-- RIGHT --> <label for="email">Email</label> <input id="email" type="email">
<!-- WRONG --> <input type="email" placeholder="Email"> <!-- RIGHT --> <label for="email">Email</label> <input id="email" type="email" placeholder="you@example.com">
Note placeholder still has a role — but as an EXAMPLE format, not as the label itself.
<!-- WRONG (typo in id, association broken) --> <label for="emial">Email</label> <input id="email" type="email"> <!-- RIGHT --> <label for="email">Email</label> <input id="email" type="email">
"Name" alone is ambiguous in a form that asks for several names. "First name" / "Last name" / "Company name" each have their own role. Generic labels make voice control and screen readers ambiguous.
<!-- WRONG (* only visible, not announced) --> <label for="email">Email *</label> <input id="email" type="email"> <!-- RIGHT --> <label for="email">Email <span aria-hidden="true">*</span></label> <input id="email" type="email" required aria-required="true">
The required attribute is what screen readers announce. The asterisk is decorative for sighted users.
Want labels that look like placeholders but stay accessible? The float-label pattern:
<div class="float-label">
<input id="email" type="email" placeholder=" ">
<label for="email">Email</label>
</div>
<style>
.float-label {
position: relative;
}
.float-label input {
padding: 18px 12px 6px;
}
.float-label label {
position: absolute;
top: 16px;
left: 12px;
transition: all 0.2s;
pointer-events: none;
}
.float-label input:focus + label,
.float-label input:not(:placeholder-shown) + label {
top: 4px;
font-size: 12px;
color: #7c3aed;
}
</style>
Label visible initially. When user focuses or types, label animates up to make room. Real <label> element preserved — programmatic association intact.
Open VoiceOver (Cmd+F5 on macOS) or NVDA (Windows free download). Tab through your form. Every field should announce:
Anything that announces just "edit text, blank" is a missing or broken label.