Tab should move through a page in an order that makes sense. Misplaced error summaries, hidden drawers and lost focus after a step change break that.
Success Criterion 2.4.3 applies when a page can be navigated in sequence and that sequence affects meaning or operation. Focusable components must receive focus in an order that preserves meaning and operability.
The order does not have to match the visual layout exactly, and it can differ from the reading order as long as the page stays understandable and operable. What it must not do is jump somewhere that breaks the relationships on the page, such as moving from a field to the "Submit" button before the remaining fields, or landing inside content that is not visible.
Arrow-key movement inside a composite widget such as a tree or tab list is not sequential navigation under this criterion.
School district enrollment and permit applications have many steps, which makes these problems frequent and costly: someone can fill in half a form while focus sits on the wrong part of the page.
After a failed submit, the summary of problems should come before the form and receive focus.
form.addEventListener("submit", e => {
const errors = validate();
if (errors.length) {
e.preventDefault();
document.body.append(buildSummary(errors)); // far below the form
}
});form.addEventListener("submit", e => {
const errors = validate();
if (errors.length) {
e.preventDefault();
const summary = buildSummary(errors);
summary.tabIndex = -1;
form.before(summary);
summary.focus();
}
});Moving a drawer off screen with a transform does not remove it from keyboard navigation. Focus can land on invisible links.
.drawer { transform: translateX(-100%); }
.drawer.open { transform: none; }.drawer { transform: translateX(-100%); visibility: hidden; }
.drawer.open { transform: none; visibility: visible; }
/* or: <aside class="drawer" inert> while closed */When a step is replaced, focus should go to the new step's heading, so the next Tab reaches its first field.
nextBtn.addEventListener("click", () => {
stepContainer.innerHTML = renderStep(2); // focus is lost
});nextBtn.addEventListener("click", () => {
stepContainer.innerHTML = renderStep(2);
const h = stepContainer.querySelector("h2");
h.tabIndex = -1;
h.focus();
});Screen readers and switch access follow the semantic order, which can diverge from the visual layout. The Compose properties used below need Compose UI 1.5 or later and may require an experimental opt-in. SwiftUI:
ZStack {
PayButton()
AmountDue(amount)
}ZStack {
PayButton().accessibilitySortPriority(1)
AmountDue(amount).accessibilitySortPriority(2)
}
.accessibilityElement(children: .contain)Jetpack Compose:
Box {
PayButton(onPay, Modifier.align(Alignment.BottomEnd))
AmountDue(amount)
}Box(Modifier.semantics { isTraversalGroup = true }) {
AmountDue(amount, Modifier.semantics { traversalIndex = 0f })
PayButton(
onPay,
Modifier.align(Alignment.BottomEnd)
.semantics { traversalIndex = 1f }
)
}Common mistakes are positive tabindex values that reorder the page, and CSS that reorders elements visually while the source order stays different. Tools can flag positive tabindex, but they cannot judge whether the order is meaningful.
For mobile screens, review the order with a screen reader on, not just by looking. See mobile apps for how the Title II rule applies.