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 whose for matches the select’s id
  • an aria-label with text, an aria-labelledby that resolves to text, or a title with text
  • role="none" or role="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

EN 301 549 clauses

The EN 301 549 clause of each criterion, by version of the standard
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)
Before and after Example data

sections/main-collection.liquid Changes to sections/main-collection.liquid: 2 added, 2 removed

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

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)
turns the variant group titles into labels of their selects; select-name no longer fires Example data

Resources/views/storefront/page/product-detail/configurator.html.twig Changes to Resources/views/storefront/page/product-detail/configurator.html.twig: 1 added, 1 removed

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)
ties an unassociated coupon label to its field, as in the fixed cart Example data

woocommerce/cart/cart.php Changes to woocommerce/cart/cart.php: 1 added, 1 removed

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)
turns the "Sort By" text into the sorter label and validates Example data

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

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 check

Or 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

Write to Validiscan

For a free check, give the address of your shop and its platform.

Validiscan uses what you send to answer you. Privacy notice