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)
Before and after Example data

sections/header.liquid Changes to sections/header.liquid: 1 added, 1 removed

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)
removes tabindex="1" from the buy button; vs-focus-order and axe tabindex stop firing Example data

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

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)
removes tabindex="1" from the header search field of the fixture theme Example data

header.php Changes to header.php: 1 added, 1 removed

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&hellip;', '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&hellip;', '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)
removes tabindex 1-4 from the login form and the CAPTCHA and validates Example data

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

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

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 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