Select element must have an accessible name
Drop-down menus for size, colour, quantity, country or sorting need a name. Without one, screen reader users hear only "combo box" and cannot tell what they are choosing, which leads to wrong orders or blocked checkouts.
- Rule id
- select-name
- Source
- axe-core 4.13.0
- Impact
- Critical
- Fix tier
- ASSISTED
- on Shopify, Shopware, WooCommerce and Magento
- Status
- runs
What the check does
Ensure select element has an accessible name
The rule tests every <select> that assistive technology can reach; one hidden with display: none is skipped. A select passes with any of these:
- a wrapping
<label>with text of its own - a
<label>with text whoseformatches the select’sid - an
aria-labelwith text, anaria-labelledbythat resolves to text, or atitlewith text role="none"orrole="presentation", but only on a select that cannot take focus, such as a disabled one
Option text never counts: a <label> or aria-labelledby whose only text comes from the select’s own options fails.
A <label for> hidden from assistive technology fails the select unless another source still names it. A visually hidden label that screen readers can read is accepted.
When no source names the select and one of these checks cannot complete, the result lands in axe-core’s needs-review list, which Validiscan’s scanner does not currently turn into findings. The rule does not judge whether the name tells a customer what they are choosing.
How to fix it
Associate a <label> with every <select> through matching for and id attributes, or give it aria-label or aria-labelledby, for example <label for="size">Size</label>. Take the label text from the translated option names of the shop.
WCAG success criteria
- 4.1.2 Name, Role, Value, level A
EN 301 549 clauses
| WCAG criterion | Version 3.2.1 | Version 4.1.1 |
|---|---|---|
| 4.1.2 | 9.4.1.2 | 9.4.1.2 |
Recipes that list this rule
This rule has the tier ASSISTED. In a project, a finding of it takes the ASSISTED way, and these recipes are not applied to it.
Shopify: recipe label-association
Show the before and after (1)
sections/main-collection.liquid Changes to sections/main-collection.liquid: 2 added, 2 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 64 | 64 | <div class="collection__products"> | |
| 65 | 65 | <div class="collection__toolbar"> | |
| 66 | 66 | <div class="collection__sort"> | |
| 67 | Removed | <label class="collection__sort-label">Sort by</label> | |
| 68 | Removed | <select name="sort_by" class="select__select" form="FacetFiltersForm"> | |
| 67 | Added | <label class="collection__sort-label" for="SortBy-{{ section.id }}">Sort by</label> | |
| 68 | Added | <select name="sort_by" class="select__select" form="FacetFiltersForm" id="SortBy-{{ section.id }}"> | |
| 69 | 69 | <option value="manual" selected>Featured</option> | |
| 70 | 70 | <option value="best-selling">Best selling</option> | |
| 71 | 71 | <option value="price-ascending">Price, low to high</option> |
sections/main-product.liquid Changes to sections/main-product.liquid: 2 added, 2 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 94 | 94 | </div> | |
| 95 | 95 | {%- else %} | |
| 96 | 96 | <div class="product-form__input product-form__input--dropdown"> | |
| 97 | Removed | <p class="form__label">{{ option.name | escape }}</p> | |
| 97 | Added | <p class="form__label" id="Options-{{ section.id }}-{{ forloop.index0 }}-label">{{ option.name | escape }}</p> | |
| 98 | 98 | <div class="select"> | |
| 99 | Removed | <select class="select__select" name="options[{{ option.name | escape }}]" data-option-index="{{ option_index }}"> | |
| 99 | Added | <select class="select__select" name="options[{{ option.name | escape }}]" data-option-index="{{ option_index }}" aria-labelledby="Options-{{ section.id }}-{{ forloop.index0 }}-label"> | |
| 100 | 100 | {%- for value in option.values %} | |
| 101 | 101 | <option value="{{ value | escape }}"{% if value == option.selected_value %} selected{% endif %}>{{ value | escape }}</option> | |
| 102 | 102 | {%- endfor %} |
Shopware: recipe label-association
Show the before and after (1)
Resources/views/storefront/page/product-detail/configurator.html.twig Changes to Resources/views/storefront/page/product-detail/configurator.html.twig: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 13 | 13 | {% for group in product.configurator %} | |
| 14 | 14 | {% if group.display == 'select' %} | |
| 15 | 15 | <div class="product-detail-configurator-group"> | |
| 16 | Removed | <div class="product-detail-configurator-group-title">{{ group.name }}</div> | |
| 16 | Added | <label class="product-detail-configurator-group-title" for="configurator-{{ group.id }}">{{ group.name }}</label> | |
| 17 | 17 | <select class="form-select product-detail-configurator-select-input" | |
| 18 | 18 | id="configurator-{{ group.id }}" | |
| 19 | 19 | name="{{ group.id }}"{% if group.price|default(false) %} |
WooCommerce: recipe label-association
Show the before and after (1)
woocommerce/cart/cart.php Changes to woocommerce/cart/cart.php: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 62 | 62 | <tr> | |
| 63 | 63 | <td colspan="6" class="actions"> | |
| 64 | 64 | <div class="coupon"> | |
| 65 | Removed | <label class="screen-reader-text"><?php esc_html_e( 'Coupon:', 'woocommerce' ); ?></label> | |
| 65 | Added | <label class="screen-reader-text" for="coupon_code"><?php esc_html_e( 'Coupon:', 'woocommerce' ); ?></label> | |
| 66 | 66 | <input type="text" name="coupon_code" class="input-text" id="coupon_code" value="" placeholder="<?php esc_attr_e( 'Coupon code', 'woocommerce' ); ?>"> | |
| 67 | 67 | <button type="submit" class="button" name="apply_coupon" value="<?php esc_attr_e( 'Apply coupon', 'woocommerce' ); ?>"><?php esc_html_e( 'Apply coupon', 'woocommerce' ); ?></button> | |
| 68 | 68 | </div> |
Magento: recipe label-association
Show the before and after (1)
app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/list/toolbar.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Catalog/templates/product/list/toolbar.phtml: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 12 | 12 | <div class="toolbar-sorter sorter"> | |
| 13 | 13 | <!-- VALIDISCAN-SEEDED select-name + vs-forms-ecommerce: the "Sort By" text is a span, not a label for the select --> | |
| 14 | 14 | <?php /* Fixed variant: <label class="sorter-label" for="sorter">Sort By</label>. */ ?> | |
| 15 | Removed | <span class="sorter-label"><?= $escaper->escapeHtml(__('Sort By')) ?></span> | |
| 15 | Added | <label class="sorter-label" for="sorter"><?= $escaper->escapeHtml(__('Sort By')) ?></label> | |
| 16 | 16 | <select id="sorter" data-role="sorter" class="sorter-options"> | |
| 17 | 17 | <?php foreach ($block->getAvailableOrders() as $key => $order): ?> | |
| 18 | 18 | <option value="<?= $escaper->escapeHtmlAttr($key) ?>"<?= $block->isOrderCurrent($key) ? ' selected' : '' ?>><?= $escaper->escapeHtml(__($order)) ?></option> |
Start with a free check of your shop.
Send your shop’s address. Validiscan scans it and replies with every failure the scan can prove, with the page and the code.
Request a free checkOr write to info@validiscan.com.
What the free check covers, and what it leaves out- You send
- your shop’s address
- Pages scanned
- up to 15
- You get
- every failure the scan can prove, with the page and the code
- On your shop
- public pages only; no login, nothing bought, no forms sent
- What it is not
- a full audit