PLAINFINCH learn by building

Project: a keyboard-friendly booking page

You will fix a bike shop booking page so someone using only a keyboard can reach every part and book a service.

What does this project ask you to fix?

The starting page is a booking form for Spoke and Chain. It looks tidy with a mouse and it is close to unusable with a keyboard. Five separate barriers stack up, one from each of the last few lessons: no skip link, a positive tabindex, focus styles removed, a script that traps focus, and a booking “button” built from a <div>.

Fix them one at a time and check each in the views as you go. The checks on this page test behaviour as well as markup: they press Tab, press Enter on your skip link, try to leave the phone box, and look for a working button. The last check asks for an empty Checks list. Small details, like the skip link’s href matching the id on <main>, decide whether the page works.

Where should you start?

Start by using the page the way a keyboard user would. Open the **Keyboard** view and turn on the numbers, then press Tab from the top. The phone number box is stop 1, ahead of the five menu links, because of tabindex="1". Nothing on screen shows which control has focus. Once you are in the phone box, Tab refuses to leave. And the Book my service box never appears in the list.

Write the list down before you change anything. Tab order, focus visibility, traps and missing controls are different problems with different fixes, and a page often has several at once. Working from a list keeps you from fixing the one you noticed first and missing the rest. It also matches how real audits go: test, record, fix, test again.

How should the Tab order run?

Tab order should follow the reading order: the skip link, then the menu, then the form from top to bottom. That is what 2.4.3 Focus Order asks for, an order that keeps meaning and makes sense. The browser does this for free when focusable elements appear in the source in the right order and nothing overrides it.

A positive tabindex overrides it. Every element with tabindex="1" or higher is visited before any element without one, so the phone box jumps ahead of the whole menu. People who reach it first have no idea what form they are in. Delete the attribute. If something is in the wrong place, move it in the HTML instead. Use tabindex="0" only to make a custom widget focusable, and tabindex="-1" only for elements that scripts focus, such as a heading after a page update.

FigureTab order on the Spoke and Chain booking page. Before: the phone box with tabindex 1 is stop 1, ahead of the five menu links, and the Book my service div is never reached. After: skip link, five menu links, Service, Day, Phone number, then the Book my service button.Tab order on the Spoke and Chain booking page. Before: the phone box with tabindex 1 is stop 1, ahead of the five menu links, and the Book my service div is never reached. After: skip link, five menu links, Service, Day, Phone number, then the Book my service button.
On the left, tabindex="1" pulls the phone box ahead of the menu and the div button never gets a stop. On the right, with tabindex removed and a real button, the ten stops follow the reading order.

How do you add a skip link that works?

Put a link to the main content as the very first element in <body>, before the header. The link points at an id, here #main on the <main> element, so pressing Enter moves focus into the content and the next Tab continues from there. That meets 2.4.1 Bypass Blocks, which asks for a way past blocks repeated on every page, like the menu.

Many sites hide the link until it is focused, which keeps the design clean while sighted keyboard users still see it appear. Move it off screen with top or a transform and bring it back on :focus. Never hide it with display: none or visibility: hidden, since those remove it from the Tab order entirely. Make sure its colours pass contrast in both states, and that it shows above any sticky header.

Current browsers move the Tab starting point to the target when a skip link is followed, so the next Tab continues inside <main>. Focus itself lands on a plain <main> only if it is focusable, and some screen readers announce the jump poorly without that. Adding tabindex="-1" to the target makes focus land on it everywhere. This course’s engine behaves like the helpful case, so test the real thing in a couple of browsers.

<a class="skip" href="#main">Skip to main content</a>

.skip { position: absolute; left: 8px; top: -60px; padding: 8px 12px; background: #1b1f24; color: #fff; }
.skip:focus { top: 8px; }

Why bring focus styles back?

The rule a:focus, button:focus, input:focus, select:focus { outline: none; } removes the only signal sighted keyboard users have. Without it they press Tab and have to guess where they are. That fails 2.4.7 Focus Visible. The usual reason for the rule is a designer who disliked the ring after a mouse click.

The :focus-visible pseudo-class answers that complaint. Browsers apply it when focus should be shown, after a key press or in a text field, and usually skip it after a mouse click on a button. Replace the old rule with one :focus-visible outline for the whole page. Use a colour with at least 3:1 contrast against the background, as 1.4.11 Non-text Contrast asks, and add outline-offset so the ring does not sit on top of the border.

:focus-visible { outline: 3px solid #1d3fbf; outline-offset: 2px; }

What is wrong with the phone box script?

The script listens for focusout and, when fewer than ten digits have been typed, puts focus straight back into the field. Its author wanted complete phone numbers. The effect is a keyboard trap that fails 2.1.2 No Keyboard Trap. Someone who wants to check the date again, or who doesn’t have a phone, can’t go anywhere. A mouse user can still click away, which is why this kind of bug survives so long.

Delete the script. Check the number when the form is submitted, show a message beside the field and let people decide what to fix first. The form lessons that follow show how to write and connect those messages.

Why does Book my service need to be a button?

A <div> with onclick responds to mouse clicks and nothing else. It is not in the Tab order, a screen reader announces it as plain text, and Enter and Space do nothing. The form has no submit control a keyboard user can press, so nobody using the keyboard can book.

Use <button type="submit">. It is focusable, a screen reader says “button, Book my service”, Enter and Space activate it, and it submits the form without any script. Keep the class so the styling stays, add border: 0 and font: inherit, and remove the onclick. Adding role="button" and tabindex="0" to the <div> gets part of the way there, then needs a script for Enter and Space and still doesn’t submit the form.

How do you know the page is finished?

Run three passes. In **Checks**, the list should be empty of serious and moderate problems. In **Keyboard**, with numbers on, the order should read skip link, five menu links, service, day, phone, button, and every stop should show a ring on the page. Then press Tab from the very top with a real keyboard: Enter on the skip link should land you in the main content, focus should leave the phone box freely, and Enter on the button should send the form.

Finally, read the **Screen reader** view. The button line should say “button, Book my service”, and every field should have a label. Keyboard access and screen reader access overlap but are not the same: a page can pass every Tab test and still read badly. When an AI tool writes a page like this one, check the same five things, because generated layouts commonly include outline: none resets, <div> buttons and positive tabindex values copied from old examples.

Field notes

  • Tab order should follow the reading order of the page.
  • A skip link as the first Tab stop lets people jump straight to the main content.
  • Removing the focus outline hides where keyboard focus is.
  • A <div> with onclick only works with a mouse, so use a real <button>.

Easy to get wrong

Positive tabindex values send focus to that element before everything else on the page. Put elements in the right order in the HTML instead.

Removing the outline leaves keyboard users guessing where focus is. :focus-visible shows it for keyboard use and usually hides it after mouse clicks.

An element with display: none can’t receive focus, so the skip link disappears from the Tab order. Move it off screen and bring it back on focus.

A <div> can’t be reached with Tab or pressed with Enter or Space. A submit button does all of that and sends the form with no script.

Questions people ask

Does every page need a skip link?

Pages with a block of links before the content, like a menu repeated on every page, should have one. A simple page with a heading and a few paragraphs doesn’t. Headings and landmarks help screen reader users skip too, and a skip link helps sighted keyboard users, who have no heading shortcut.

Why does the engine accept a skip link without tabindex on the target?

Current browsers move the sequential focus starting point to the target of a followed in-page link, so the next Tab continues from there in most cases. Adding tabindex="-1" to the target makes the behaviour consistent, and it does no harm.

Should the skip link always be visible?

It can be. A visible skip link helps people who don’t expect one. Hiding it until it has focus is common and allowed, as long as it really appears on focus and has good contrast.

How long should a keyboard test take?

For a page like this one, a couple of minutes: Tab to the end, Shift+Tab back, open and close anything that opens, and submit the form once.

Keep going