Elements should not have tabindex greater than zero
A tabindex greater than zero forces an element to the front of the keyboard order. Keyboard and screen reader users then jump around the page in an unexpected order, for example from the header straight to a newsletter field, and can lose their way to the cart or checkout.
- Rule id
- tabindex
- Source
- axe-core 4.13.0
- Impact
- Serious
- Fix tier
- AUTO
- on Shopify, Shopware, WooCommerce and Magento
- Status
- runs
What the check does
Ensure tabindex attribute values are not greater than 0
The rule tests every element with a tabindex attribute, whatever the element, unless it is hidden from assistive technology. It reads the attribute value, not the DOM property. axe-core trims the value and takes its leading whole number, with an optional sign, so tabindex="2.5" counts as 2 and tabindex="+1" as 1. A value without a leading number passes, as do 0 and negative values.
axe-core tags the rule best practice, and it maps to no WCAG success criterion. Validiscan’s scanner runs it, by default, in a separate best-practice pass that keeps only failures. The finding is stored as best practice and left out of the conformance tables and the remediation progress figure. A positive tabindex on a visible element that can take focus can also be reported by vs-focus-order under success criterion 2.4.3.
The rule does not judge whether the resulting tab order makes sense, or whether tabindex="0" and tabindex="-1" sit on the right elements; a person checks that with the keyboard.
How to fix it
Remove positive tabindex values or change them to tabindex="0", and arrange the source order of the template so the natural tab order follows the visual layout.
WCAG success criteria
Best practice: not tied to a WCAG success criterion.
Where a recipe fixes it
Shopify: recipe remove-positive-tabindex
Show the before and after (1)
sections/header.liquid Changes to sections/header.liquid: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 37 | 37 | ||
| 38 | 38 | <div class="header__search"> | |
| 39 | 39 | <form action="{{ routes.search_url }}" method="get" role="search" class="search"> | |
| 40 | Removed | <input class="search__input field__input" type="search" name="q" value="" placeholder="{{ 'general.search.placeholder' | t }}" tabindex="1"> | |
| 40 | Added | <input class="search__input field__input" type="search" name="q" value="" placeholder="{{ 'general.search.placeholder' | t }}"> | |
| 41 | 41 | <button type="submit" class="search__button"> | |
| 42 | 42 | {%- render 'icon', icon: 'search', size: 20 -%} | |
| 43 | 43 | </button> |
Shopware: recipe remove-positive-tabindex
Show the before and after (1)
Resources/views/storefront/page/product-detail/buy-widget.html.twig Changes to Resources/views/storefront/page/product-detail/buy-widget.html.twig: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 82 | 82 | </div> | |
| 83 | 83 | <div class="buy-widget-submit"> | |
| 84 | 84 | <button class="btn btn-primary btn-buy" | |
| 85 | Removed | title="{{ 'detail.addProduct'|trans }}" tabindex="1"> | |
| 85 | Added | title="{{ 'detail.addProduct'|trans }}"> | |
| 86 | 86 | {{ 'detail.addProduct'|trans }} | |
| 87 | 87 | </button> | |
| 88 | 88 | </div> |
WooCommerce: recipe remove-positive-tabindex
Show the before and after (1)
header.php Changes to header.php: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 49 | 49 | <form role="search" method="get" class="woocommerce-product-search" action="<?php echo esc_url( home_url( '/search' ) ); ?>"> | |
| 50 | 50 | <label class="screen-reader-text" for="woocommerce-product-search-field-0"><?php esc_html_e( 'Search for:', 'woocommerce' ); ?></label> | |
| 51 | 51 | <?php // VALIDISCAN-SEED vs-focus-order + tabindex: tabindex="1" "so the search is always the first stop". ?> | |
| 52 | Removed | <input type="search" id="woocommerce-product-search-field-0" class="search-field" placeholder="<?php echo esc_attr__( 'Search products…', 'woocommerce' ); ?>" value="<?php echo get_search_query(); ?>" name="s" tabindex="1"> | |
| 52 | Added | <input type="search" id="woocommerce-product-search-field-0" class="search-field" placeholder="<?php echo esc_attr__( 'Search products…', 'woocommerce' ); ?>" value="<?php echo get_search_query(); ?>" name="s"> | |
| 53 | 53 | <?php // VALIDISCAN-SEED button-name: icon-only submit button without text. ?> | |
| 54 | 54 | <button type="submit" value="<?php echo esc_attr_x( 'Search', 'submit button', 'woocommerce' ); ?>" class="kb-search-submit"><?php kb_icon( 'search' ); ?></button> | |
| 55 | 55 | <input type="hidden" name="post_type" value="product"> |
Magento: recipe remove-positive-tabindex
Show the before and after (1)
app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/captcha-math.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/captcha-math.phtml: 1 added, 1 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 14 | 14 | <div class="field captcha required" role="group" aria-labelledby="captcha-question" data-captcha-type="math"> | |
| 15 | 15 | <label for="captcha_user_login" class="label" id="captcha-question"><span><?= $escaper->escapeHtml(__('Security question: what is %1 + %2?', $a, $b)) ?></span></label> | |
| 16 | 16 | <div class="control captcha"> | |
| 17 | Removed | <input name="captcha[user_login]" type="text" class="input-text required-entry" id="captcha_user_login" autocomplete="off" inputmode="numeric" tabindex="3" data-captcha-answer-hash="<?= $escaper->escapeHtmlAttr(md5((string) ($a + $b))) ?>"> | |
| 17 | Added | <input name="captcha[user_login]" type="text" class="input-text required-entry" id="captcha_user_login" autocomplete="off" inputmode="numeric" data-captcha-answer-hash="<?= $escaper->escapeHtmlAttr(md5((string) ($a + $b))) ?>"> | |
| 18 | 18 | </div> | |
| 19 | 19 | </div> |
app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml Changes to app/design/frontend/Validiscan/fixture/Magento_Customer/templates/form/login.phtml: 3 added, 3 removed
| Line before | Line after | Change | Code |
|---|---|---|---|
| 27 | 27 | <div class="field email required"> | |
| 28 | 28 | <label class="label" for="email"><span><?= $escaper->escapeHtml(__('Email')) ?></span></label> | |
| 29 | 29 | <div class="control"> | |
| 30 | Removed | <input name="login[username]" value="<?= $escaper->escapeHtmlAttr($username) ?>" autocomplete="off" id="email" type="email" class="input-text mage-error" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" aria-invalid="true" tabindex="1" onpaste="return false;" data-mage-init='{"mage/trim-input":{}}'> | |
| 30 | Added | <input name="login[username]" value="<?= $escaper->escapeHtmlAttr($username) ?>" autocomplete="off" id="email" type="email" class="input-text mage-error" title="<?= $escaper->escapeHtmlAttr(__('Email')) ?>" aria-invalid="true" onpaste="return false;" data-mage-init='{"mage/trim-input":{}}'> | |
| 31 | 31 | <div for="email" generated="true" class="mage-error" id="email-error"><?= $escaper->escapeHtml(__('Please enter a valid email address (Ex: johndoe@domain.com).')) ?></div> | |
| 32 | 32 | </div> | |
| 33 | 33 | </div> | |
| 35 | 35 | <div class="field password required"> | |
| 36 | 36 | <label for="pass" class="label"><span><?= $escaper->escapeHtml(__('Password')) ?></span></label> | |
| 37 | 37 | <div class="control"> | |
| 38 | Removed | <input name="login[password]" type="password" autocomplete="off" class="input-text" id="pass" title="<?= $escaper->escapeHtmlAttr(__('Password')) ?>" tabindex="2" onpaste="return false;" oncopy="return false;"> | |
| 38 | Added | <input name="login[password]" type="password" autocomplete="off" class="input-text" id="pass" title="<?= $escaper->escapeHtmlAttr(__('Password')) ?>" onpaste="return false;" oncopy="return false;"> | |
| 39 | 39 | </div> | |
| 40 | 40 | </div> | |
| 41 | 41 | <!-- VALIDISCAN-SEEDED vs-accessible-auth: arithmetic CAPTCHA with no alternative --> | |
| 42 | 42 | <?= $block->getChildHtml('form_additional_info') /* math CAPTCHA module: div.field.captcha, input#captcha_user_login tabindex="3" */ ?> | |
| 43 | 43 | <div class="actions-toolbar"> | |
| 44 | Removed | <div class="primary"><button type="submit" class="action login primary" name="send" id="send2" tabindex="4"><span><?= $escaper->escapeHtml(__('Sign In')) ?></span></button></div> | |
| 44 | Added | <div class="primary"><button type="submit" class="action login primary" name="send" id="send2"><span><?= $escaper->escapeHtml(__('Sign In')) ?></span></button></div> | |
| 45 | 45 | <div class="secondary"><a class="action remind" href="<?= $escaper->escapeUrl($block->getUrl('contact')) ?>"><span><?= $escaper->escapeHtml(__('Forgot Your Password? Contact us')) ?></span></a></div> | |
| 46 | 46 | </div> | |
| 47 | 47 | </fieldset> |
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