Fix

A positive tabindex is hijacking your keyboard order

Somewhere in your markup an element says tabindex="1", and from that moment the Tab key stops following the page. Focus jumps there first from everywhere — then snaps back to the top to sweep everything else.

Auto-applied WCAG 2.4.3

Who this locks out

A tabindex of 1 drags keyboard focus out of reading order on every page it touches.

  • Keyboard-only shoppers, thrown across the page mid-checkout
  • Screen-reader users, whose focus path contradicts the reading order
  • Screen-magnifier users, whose zoomed viewport lurches wherever focus jumps

The transform

Before

<a href="/checkout" class="cart-link" tabindex="1">Checkout</a>

After

<a href="/checkout" class="cart-link">Checkout</a>

Detection

How this is tested here

Every scan runs three independent engines and 19 custom checks against your rendered pages, and records which engine flagged what. These are the real rules behind this fix family.

WCAG success criteria this family covers, and the engine rules that detect them. Generated from the engine's own tables — never edited by hand.
Criterion Level Detected by What we do
2.4.3 Focus Order Most affects: People who are blind · People with motor impairments · People with cognitive disabilities A no automated rule — human audit auto

What Klarvo Access does

Klarvo finds every element served with a positive tabindex, removes the attribute server-side in the HTML your store serves so natural DOM order rules again, then re-fetches the page and confirms the attribute is gone.

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

What a positive tabindex does to your store

The tabindex attribute accepts three kinds of value, and only one of them is dangerous. tabindex="0" adds an element to the keyboard order exactly where it sits in the document. tabindex="-1" lets a script move focus to an element — a skip-link target, an error summary — without adding a tab stop. A positive value does something else entirely: it pulls the element out of the document’s order and into a priority queue.

The browser visits that queue first. Every element with a positive tabindex, in ascending value order, across the whole page — and only then does focus return to the top of the document to walk everything else in source order. One tabindex="1" on a newsletter field in your footer means the first Tab press on every page that prints that footer — home, category, product, checkout — lands below the fold, in the footer, on a field the shopper cannot see. The second press abandons them back at the top of the header. Nothing about the page looks wrong. The order of it is scrambled.

This is what WCAG 2.4.3 Focus Order (Level A) exists to prevent: focusable components should receive focus in an order that preserves meaning and operability. The W3C documents the positive-tabindex version of the failure in F44, and its recommended technique is disarmingly plain — make the DOM order match the visual order (C27) and let the browser do the rest.

The attribute rarely arrives by malice. It comes from an old tutorial that numbered a form’s fields 1 to 9; from a slider or carousel licensed a decade ago; from a form plugin’s per-field “tab index” setting that someone filled in once; from markup pasted into a Custom HTML block; from a well-meant attempt to make the buy button “first”. It survives every redesign because it changes nothing visually, so nobody ever sees it to question it.

The people who meet it are precisely the ones with the least slack to absorb it:

  • A keyboard-only shopper — tremor, RSI, no usable mouse — typing a delivery address at checkout. Tab should carry them from the town field to the postcode; instead it flings them to whichever element claimed priority, and every field after it is a small act of navigation rather than a keystroke.
  • A screen-reader user, for whom the page is the order it is announced in. When focus order contradicts reading order, the mental map collapses: the thing they just heard is not the thing they just reached.
  • A screen-magnifier user working at several times normal size, whose viewport follows focus. A focus leap is not an abstraction for them — it is the whole visible screen lurching to a different part of the page, twice, for one press of Tab.

How it is detected here

None of the third-party engines in the scan decides this family. axe-core does carry a tabindex rule for the pattern, but files it as a best practice rather than mapping it to the criterion, so it is corroboration, not the trigger.

The finding comes from Klarvo’s own deterministic check over the raw HTML your server sends — fetched the way a visitor fetches it, not reconstructed from a rendered approximation. Every opening tag in the served source is parsed; where a tabindex attribute is present, its value is read as an integer. "3", "+3" and "007" all count. Anything greater than zero is the finding, and it is logged as serious, because a single attribute reorders the entire page. tabindex="0" and tabindex="-1" are never flagged — they are the two values the attribute is for.

There is no heuristic and no judgement call anywhere in that path: either the served HTML carries a parsed positive tabindex or it does not. That is also what makes the fix safe to automate.

What Klarvo does about it

This family is auto tier, and the fix is the most conservative one available: remove the attribute, so the browser’s natural document order rules again.

The removal is targeted, not pattern-matched. The exact opening tag is fingerprinted byte for byte, with an ordinal to distinguish identical twins, and the rule is scoped to the exact page it was found on. The plugin then performs a targeted tag-rewrite in the HTML your store serves: that one element loses its tabindex attribute, and nothing else about the tag — classes, ids, data attributes, anything a script hooks — is touched. This happens server-side, in the markup that leaves your server, never as a script repainting the page in the visitor’s browser. What a screen reader, a browser and a scanner receive is the same corrected document.

Removal, rather than rewriting the value to 0, is deliberate. A link, button or input is focusable by nature; taking the attribute away restores the browser’s own default instead of substituting a new opinion about where the element belongs. The element stays exactly where it is in the markup — it simply stops queue-jumping.

An automated fix is recorded with evidence, not assumed: after applying, the engine independently re-fetches the page, finds the element again by its identity, and confirms the attribute is genuinely absent from the newly served source. Only then is the finding recorded as fixed.

How to fix it yourself

Find every instance first

Open the browser console on your home page, shop page, a product, the basket and checkout, and run:

[...document.querySelectorAll('[tabindex]')]
	.filter((el) => parseInt(el.getAttribute('tabindex'), 10) > 0)
	.forEach((el) => console.log(el.getAttribute('tabindex'), el.outerHTML.slice(0, 120)));

Anything it prints is a queue-jumper. Cross-check against the served source rather than only the live DOM, because a script can add or remove the attribute after load:

curl -s https://www.example.com/ | grep -o 'tabindex="[^"]*"' | sort | uniq -c

Trace where each one is printed

  • Theme templates. Search the theme for the attribute: grep -rn 'tabindex' wp-content/themes/your-theme/. Edit a child theme, never the parent.
  • Form plugins. Several form builders expose a per-field “tab index” box. Clear it; the plugin then omits the attribute and the form falls back to document order, which is what you wanted anyway.
  • Page builders. Look in the widget’s attributes panel — builders that let you add custom attributes are the usual carrier. Delete the entry there so the fix survives re-saves.
  • Content. Markup pasted into a Custom HTML block keeps whatever it arrived with. Edit the block directly.

A stopgap filter while you chase the source

If the markup comes from something you cannot edit today, a child-theme filter can strip positive values from block output:

add_filter( 'render_block', function ( $content ) {
	return preg_replace( '/\stabindex="\+?0*[1-9][0-9]*"/', '', $content );
} );

Treat this as a tourniquet, not a repair: it is pattern-matching on HTML, it only reaches block-rendered output, and it should be deleted once the source is corrected. (Klarvo’s own removal is byte-targeted at a fingerprinted element for exactly this reason — a pattern is a guess, an identity is not.)

Use the two legitimate values

Where a custom widget genuinely needs to be keyboard-operable — a hand-rolled disclosure, a card that acts as a control — give it tabindex="0" along with a real role and key handling, or better, build it on a <button>. Use tabindex="-1" for programmatic focus targets: the content wrapper a skip link jumps to, or the error summary you move focus to when checkout validation fails.

Fix the order in the source, not with numbers

If someone reached for tabindex="1" because the important element sits too late in the page, the durable fix is to move it in the markup. In WooCommerce, template hooks make this a priority change rather than surgery — for instance, reordering what appears in the product summary:

remove_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_meta', 40 );
add_action( 'woocommerce_single_product_summary', 'woocommerce_template_single_meta', 25 );

And be wary of recreating the same failure with CSS: flexbox order and flex-direction: row-reverse reorder pixels while leaving the DOM — and therefore focus — where it was. That mismatch fails 2.4.3 with no tabindex in sight, which is why the W3C’s technique is to make DOM order match visual order rather than to decorate the difference.

How you know it worked

  1. Put the mouse down. Click into the address bar, then Tab through the home page, a category, a product, the basket and checkout. Focus should move in visual reading order — header, content, footer — with no leaps to a far corner of the page.
  2. View the served source (view-source or curl, not the DevTools inspector, which shows the DOM after scripts have run) and search for tabindex. The only values left should be 0 and -1.
  3. Re-scan. The positive-tabindex finding should clear on the re-fetched page — and because the check is a parse of the served bytes, a clean result means the markup is actually clean, not that a heuristic was satisfied.
  4. Re-check after updates. A theme or plugin release can reprint the attribute you removed, which is why this is monitored rather than fixed once and forgotten.

Sources: W3C WAI — Understanding SC 2.4.3: Focus Order · W3C WAI — Failure F44: using tabindex to create a tab order that does not preserve meaning and operability · W3C WAI — Technique C27: Making the DOM order match the visual order · Deque University — axe rule tabindex (axe-core 4.12) · MDN — tabindex (HTML global attribute)

FAQ

Questions this raises

Is tabindex="1" not the way to make my buy button come first?

It makes the button first at the price of making every other stop on the page unpredictable, because browsers visit all positive values in their own queue before the rest of the document. If the button should be first, put it first in the markup — document order is the mechanism that scales, and it is what WCAG 2.4.3 actually asks for.

Will removing the attribute stop the element being focusable?

A link, a button or a form field is focusable natively — removing tabindex only returns it to its natural place in the order. The rare custom widget that genuinely needs keyboard focus should carry tabindex="0", which joins the natural order and is never flagged or removed.

Does tabindex="0" get flagged too?

No. Zero and minus one are the two values the attribute is for — zero joins the natural order, minus one allows focus by script without adding a tab stop. Detection parses the value as an integer and acts only when it is greater than zero, so a legitimate skip-link target or modal container is left alone.

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 19 Klarvo checks, and tells you exactly what it found and where.