Fix
Form fields with no label
A field with no label is announced as an empty box. At checkout, that is the difference between a completed order and an abandoned basket.
Who this locks out
An unlabelled checkout field is a guess for anyone who cannot see its placement.
- Screen-reader users filling in billing and delivery details
- Voice-control users who ask for a field by its name
- Shoppers whose placeholder text vanishes the moment they type
- Anyone using strong magnification, who sees the box but not its caption
The transform
Before
<input type="text" name="billing_postcode" placeholder="Postcode"> After
<label for="billing_postcode">Postcode</label><input id="billing_postcode" type="text" name="billing_postcode"> Detection
How this is tested here
Every scan runs three independent engines and nine custom probes against your rendered pages, and records which engine flagged what. These are the real rules behind this fix family.
| Criterion | Level | Detected by | What we do |
|---|---|---|---|
| 1.3.1 Info and Relationships | A | axe-core (aria-hidden-body, aria-required-children, aria-required-parent, definition-list) · IBM Equal Access (aria_banner_label_unique, aria_banner_single, aria_child_valid) | auto |
| 3.3.2 Labels or Instructions | A | axe-core (form-field-multiple-labels) · IBM Equal Access (element_accesskey_labelled, input_label_after, input_label_before) | auto |
| 4.1.2 Name, Role, Value | A | axe-core (aria-allowed-attr, aria-braille-equivalent, aria-command-name, aria-conditional-attr) · IBM Equal Access (aria_accessiblename_exists, aria_activedescendant_valid, aria_attribute_allowed) | auto |
This family is keyed to the axe rules label</code>, <code>select-name in the
engine's own category map.
What Klarvo Access does
Klarvo generates the missing label, applies it server-side in the HTML your store sends, then re-runs the same engines against the served page to confirm the field now has a name.
Auto-applied
Generated, then applied server-side in the HTML your store actually serves — and independently re-validated in the served page before it counts as resolved.
An automated finding is evidence, not a certificate. Conformance is certified only by the professional audit — how that works.
Do it yourself
Fixing this by hand
An unlabelled field is a guess
A screen reader reads your checkout in the order the HTML is written. A labelled field announces “Email address, edit text”. A field with no label announces “edit text, blank” — and your customer is now typing into an anonymous box.
In a WooCommerce store it turns up in four places, all on the path to an order:
- Checkout. Billing and delivery fields added by a snippet or an extension, where a placeholder is doing the labelling work: “Postcode”, “Phone”, “VAT number”.
- Basket. The coupon code box, and the quantity input on each row.
- Search. A bare
<input type="search">in the header with the placeholder “Search products” and a magnifying-glass button. - Product page. The quantity input, and size or colour dropdowns where a theme has replaced WooCommerce’s own markup.
The people this stops are not hypothetical. A customer using NVDA or VoiceOver tabs into billing and hears a run of identical, nameless fields; fill them in the wrong order and the reward is “Billing postcode is a required field” for a field they cannot identify. A customer using voice control says “click Postcode” and nothing happens, because the command matches a field’s accessible name and this field hasn’t got one. And once anyone types, the placeholder is gone, so an interrupted shopper returns to a column of filled boxes with nothing to say what each one held.
How Klarvo detects it
Klarvo renders the page in a real browser, then runs three independent engines over the result.
axe-core 4.12.0 carries the two rules this family is keyed to: label (“Form
elements must have labels”) and select-name (“Select element must have an accessible
name”). Within the same criteria it also flags form-field-multiple-labels (two labels
on one field is as ambiguous as none), aria-input-field-name and
aria-toggle-field-name for custom widgets, and duplicate-id-aria, since two
controls sharing an id break every for pointing at it.
IBM Equal Access (ACE), vendored into the engine, reaches the same elements by a
different route: input_label_exists, label_content_exists, label_ref_valid,
form_label_unique, fieldset_legend_valid, and input_label_before /
input_label_after. HTML_CodeSniffer, also vendored, flags the same criteria
again. Those criteria are WCAG 2.1 1.3.1 Info and Relationships, 3.3.2 Labels
or Instructions and 4.1.2 Name, Role, Value — all Level A, the floor rather
than the ceiling.
Cross-engine agreement matters here, so we record it plainly: when axe’s label and
ACE’s input_label_exists land on the same input, two independently written
implementations reached the same conclusion about the same element. That is the
difference between a finding and an engine quirk.
There is no bespoke Klarvo probe for this family — the criteria data records none for these three, because the engines already cover the mechanics. What no engine can judge is whether “Address 2” is the right wording for your store: detection finds absent names, not poor ones.
What Klarvo does about it
This family runs on auto. Klarvo derives the label from the field’s own evidence —
its name and id, neighbouring text, the form’s purpose — with one model proposing
the wording and a second checking it against that evidence. The label is then applied
server-side, in the HTML your store sends. It is never an overlay: no script rewrites
your page after it loads.
Afterwards the same engines re-run against the served page, independently of the model
that proposed the change, and the finding has to be gone. Where the evidence is thin —
an input whose only clue is field_3 — the proposal goes to the review queue for a
human instead of being applied quietly. An automated finding is evidence, not a
certificate; conformance is certified only by a dated human audit at a defined scope.
How to fix it yourself
Every control that accepts input needs a name in the markup. There are four legitimate ways to give it one, best first.
1. An explicit label. The for attribute matches the field’s id. This is
technique H44, and it also gives you a bigger click target:
<label for="billing_postcode">Postcode</label>
<input type="text" id="billing_postcode" name="billing_postcode" autocomplete="postal-code">
2. A wrapping label, where there is no room for a separate caption:
<label>Coupon code <input type="text" name="coupon_code"></label>
3. aria-labelledby, pointing at text already on the page (technique ARIA16) —
useful when the caption is a heading you cannot move:
<span id="qty-label">Quantity</span>
<input type="number" aria-labelledby="qty-label" name="quantity" min="1" value="1">
4. aria-label as a last resort, for a field whose purpose is obvious from an
adjacent icon. It is invisible, so it helps nobody who is not using assistive
technology. A placeholder is not on this list, and neither is title.
Keeping the label but hiding it. If the design will not carry a visible caption,
hide it with the clip technique — never display: none or visibility: hidden, which
take the label away from assistive technology as thoroughly as from the screen. WordPress and WooCommerce
both ship a .screen-reader-text class:
.screen-reader-text {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
border: 0;
}
A classic-theme search form (searchform.php in your child theme):
<form role="search" method="get" action="<?php echo esc_url( home_url( '/' ) ); ?>">
<label class="screen-reader-text" for="site-search">Search products</label>
<input type="search" id="site-search" name="s" value="<?php echo esc_attr( get_search_query() ); ?>">
<input type="hidden" name="post_type" value="product">
<button type="submit">Search</button>
</form>
If that form appears in both the header and the footer, give the two inputs different
id values — otherwise both labels point at whichever one the browser finds first.
WooCommerce checkout fields. Core fields already have labels; restore or reword
them from a child theme’s functions.php:
add_filter( 'woocommerce_checkout_fields', function ( $fields ) {
$fields['billing']['billing_phone']['label'] = __( 'Phone number', 'your-child-theme' );
$fields['order']['order_comments']['label'] = __( 'Order notes', 'your-child-theme' );
return $fields;
}, 20 );
Add new fields with WooCommerce’s own helper rather than echoing raw HTML — it takes the label as a parameter and renders the association for you:
add_action( 'woocommerce_after_order_notes', function ( $checkout ) {
woocommerce_form_field( 'delivery_instructions', array(
'type' => 'text',
'label' => __( 'Delivery instructions', 'your-child-theme' ),
'placeholder' => __( 'e.g. leave with next door', 'your-child-theme' ),
'required' => false,
), $checkout->get_value( 'delivery_instructions' ) );
} );
Template overrides are the usual culprit. WooCommerce’s own
global/quantity-input.php includes a hidden label for the quantity field. If your
theme copied that file into your-child-theme/woocommerce/global/quantity-input.php
several versions ago, diff it against the plugin’s current copy and put the <label>
back.
Grouped controls need a group name too. Delivery-method radios want a fieldset
and legend, not just individual labels — that is the 1.3.1 half of this family:
<fieldset>
<legend>Delivery method</legend>
<label><input type="radio" name="shipping_method" value="flat_rate"> Standard</label>
<label><input type="radio" name="shipping_method" value="express"> Next day</label>
</fieldset>
Block themes and the Checkout block. In a block theme there is no searchform.php
to edit: open Appearance → Editor, select the Search block in your header template
part, and give it label text. The WooCommerce Checkout block renders fields with its
own components, so extra fields belong in WooCommerce’s additional-checkout-fields
registration, which takes the label as part of the definition. An unnamed field inside
the block checkout is nearly always a third-party extension printing its own input.
How you know it worked
Re-scan the checkout, basket, search results and a variable product page, and check
the label and select-name findings are gone from all four — a fix on the product
page tells you nothing about the checkout.
Then confirm it by hand, because a name can be present and still be wrong. Tab through the checkout with VoiceOver (Cmd+F5) or NVDA: every stop should announce a name, a type, and whether it is required. Open your browser’s element inspector, pick the accessibility pane, and read the computed Name — empty is the bug, whatever the markup looks like. Then say a field’s caption to your operating system’s voice control; focus should move there.
For a fast sweep of a whole page, paste this into the browser console:
document.querySelectorAll('input:not([type=hidden]), select, textarea').forEach((el) => {
const named =
(el.id && document.querySelector(`label[for="${CSS.escape(el.id)}"]`)) ||
el.closest('label') ||
el.getAttribute('aria-label') ||
el.getAttribute('aria-labelledby');
if (!named) console.warn('No label:', el);
});
Finally, put the re-check on a schedule. Labels are lost by theme updates, by new
checkout extensions, and by the next snippet pasted into functions.php. Conformance
drifts the moment the store changes, which is why the scan is a monitor, not an errand.
Sources: W3C WAI — Understanding SC 1.3.1: Info and Relationships · W3C WAI — Understanding SC 3.3.2: Labels or Instructions · W3C WAI — Understanding SC 4.1.2: Name, Role, Value · W3C WAI Tutorials — Labeling Controls · WCAG Technique H44 — Using label elements to associate text labels with form controls · WCAG Technique ARIA16 — Using aria-labelledby to provide a name for user interface controls · WordPress Documentation — Site Editor
FAQ
Questions this raises
The placeholder already says "Email address". Isn't that a label?
No. A placeholder disappears as soon as the customer types, so nobody can check what the field wanted, and it is not reliably treated as the field's name by assistive technology or by voice control. Keep the placeholder as a hint if it helps, and add a real label as well.
Will adding labels change how my checkout looks?
Only if you want it to. A label can be visible above the field, or present in the markup and hidden from view with the clip technique. Visible labels are the better choice for everyone, but a hidden label is a legitimate fix where the design cannot move.
WooCommerce writes my checkout. Why are fields unlabelled at all?
Core WooCommerce fields ship with labels. Missing names almost always come from three places: a theme overriding a WooCommerce template and dropping the label, a snippet adding a custom field with only a placeholder, or a third-party extension rendering its own input. The scan names the element and the page, so you know which.
Find out whether your store has this one.
The free scan checks your real pages — home, product, populated basket, hydrated checkout — with all three engines and all nine probes, and tells you exactly what it found and where.