HTML — One Tag Stands In For a Feature
Fixing Tab Order by Hand
Goal
You write a screen that can be used all the way through with only the keyboard. There is no browser, but where Tab goes is computed from markup alone.
Why it matters
Keyboard accessibility is not a feature but an order. What receives focus, in what sequence it cycles, and whether it is visible where it is now. And these three never come to light if you check with a mouse — QA uses a mouse too, so it ships as is.
In particular, a positive tabindex doesn't move that one element; it splits the page into two
groups. Everything with a positive value is cycled through first, and the rest come after.
A single tabindex="1" makes that element the first stop on the page.
What to build
It is a single file, /root/work/kbd/app.html. You write the focus indicator rules in the
document's <style>.
The calculator
cd /root/work/kbd
python3 /opt/lab/checks/html-kbd-lab/focus.py app.html
It prints the Tab order with numbers and flags positive tabindex, focusable elements inside aria-hidden,
fake widgets, and broken aria-* references.
Steps
- Put a skip link in the first stop ·
tabindex="-1"onmain - Tab order = document order (no positive tabindex)
- Replace fake widgets with native elements
aria-hiddenfor decoration,hiddenfor collapsed areas- The expand button —
aria-expandedandaria-controls - The dialog —
role="dialog"· a name · excluded from the tab order while closed - The focus indicator —
:focus-visible - Save the calculator's result as
08-focus.txt - Wrap-up →
09-notes.md
Notes
- You don't use JavaScript. What this lab looks at is structure.
aria-hidden="true"doesn't block focus. The state where focus goes there but nothing is read is the worst.- On a real screen, putting the mouse away and using Tab alone for 5 minutes catches more than ten automated tools.
A skip link at the first stop
Create /root/work/kbd/app.html, and put a <a href="#main"> skip link as the first focusable element of <body>. The destination <main id="main"> must have tabindex="-1".
Run mkdir -p /root/work/kbd. If you put it after the menu, you have already passed everything there was to skip. And if the destination is an element that can't receive focus, the browser changes only the address and leaves focus where it was — tabindex="-1" isn't reached by Tab but lets it receive focus through a link.
Match the tab order to the document order
Increase the focusable elements to six or more (3 or more links and 2 or more buttons), and have no positive tabindex at all. You also must not attach tabindex="0" to an element that already receives focus.
The calculator prints them with numbers. If even one positive tabindex exists, those are cycled through all first and the rest come after. If you want to change the order, change the markup order.
Get rid of fake widgets
Include three or more button elements and give all of them a type. There must be no div with role="button", no div/span with onclick, and no link that goes nowhere, like href="#".
role changes only the label — neither focus nor Enter and Space follow. And inside a form, a button with no type becomes a submit button, and pressing it reloads the page. If it navigates, it's a link; if it does something, it's a button.
Is what you hid really hidden
Attach aria-hidden="true" to decorative elements that need no reading, and give hidden to collapsed panels. There must be no focusable element inside aria-hidden, and the buttons inside a collapsed panel must drop out of the tab order.
aria-hidden removes things only from the assistive technology tree — focus still goes there. The state where focus goes there but nothing is read is the worst. Use it only on things that receive no focus and need no reading, like icons drawn with text characters.
Announce the state of the expand button
Include a button that has aria-expanded and aria-controls. aria-controls must point to an id that exists, and if aria-expanded="false", that panel must have hidden applied.
If you change only the arrow shape, the state reaches only those who look with their eyes. aria-expanded tells the program "is it open right now." If the state and the screen diverge, one of the two is lying.
Give the dialog a name
Build a confirmation window with role="dialog", aria-modal="true", and aria-labelledby, and put two buttons in it (run and cancel). It must start out hidden, and the button that opens this window must be connected with aria-controls.
Without a name, even when it is open it is read only as "dialog" — point to a heading element with aria-labelledby. And if the buttons inside remain in the tab order while it is closed, the user sends focus somewhere invisible.
Make focus visible
In the document's <style>, write a :focus-visible rule and give it a visible outline. There must be no rule that removes the focus ring with outline: none, and the skip link must be off-screen normally and appear on :focus.
:focus also applies on a mouse click, but :focus-visible applies only when reached by keyboard — you can show the ring only to those who need it without harming the design. The single line outline: none is the line that damages accessibility the most.
Keep the tab order on record
Run focus.py and save the result as /root/work/kbd/08-focus.txt. The number of saved stops must equal the tab order of the current document, and there must be 0 findings.
python3 /opt/lab/checks/html-kbd-lab/focus.py app.html | tee 08-focus.txt. Follow along by eye to see whether the numbers match the order you see on screen — that is the check a person has to do.
What breaks the order
In /root/work/kbd/09-notes.md, write at least three lines (at least 120 characters). The text must contain tabindex, 건너뛰기, and 초점 (the last two are the Korean words for "skip" and "focus"). The document itself must also still have 0 findings.
The core is one thing — the markup decides the order. Write down in your own words why a positive tabindex breaks the whole page rather than just that one element.