{"id":"l-a11y-3-1","courseId":"web-accessibility","lang":"en","kind":"lesson","summary":"Make every Ticketly control reachable in a logical order with the keyboard, show a clear focus indicator, add a skip link, and keep focus from hiding behind sticky bars.","html":"<p>Put your mouse in a drawer and try to buy a Friday pass on Ticketly. You will discover three things in about thirty seconds. Tab takes you through forty-odd header and footer links before reaching the festival. Focus then jumps from the Saturday button to the Friday button and back to the top. And for most of the trip you can’t see where focus is at all, because someone wrote <code dir=\"ltr\">outline: none</code> in 2019 to get rid of “that ugly blue box”.</p>\n<p>Keyboard access isn’t a niche. Everyone using a screen reader on a desktop relies on the keyboard, and so do switch users, people with tremor, people whose trackpad broke, and developers who simply prefer it. WCAG 2.1.1 Keyboard (A) requires that everything works from the keyboard, and 2.1.2 (A) that focus never gets trapped.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The keys people use</h2>\n<ul>\n<li><strong>Tab / Shift+Tab</strong>: move between interactive elements (links, buttons, form fields).</li>\n<li><strong>Enter</strong>: follow a link, activate a button, submit a form.</li>\n<li><strong>Space</strong>: activate a button, toggle a checkbox, scroll the page when nothing is focused.</li>\n<li><strong>Arrow keys</strong>: move within a widget, such as radio buttons, a select, tabs or a menu.</li>\n<li><strong>Escape</strong>: close a dialog, menu or popup.</li>\n</ul>\n<p>Native elements already support all of this. Your job is mostly not to break it, and to match these conventions when you build custom widgets in <a href=\"/lesson/web-accessibility/l-a11y-3-4\">Widget Patterns</a>.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Focus order follows the DOM</h2>\n<p>Tab moves through focusable elements in <strong>source order</strong>. If the visual layout puts the “Add Saturday” button before “Add Friday” but the markup has them the other way round, focus appears to jump backwards. The fix is to put the markup in the order you want people to meet it. Be wary of CSS that rearranges things visually, such as <code dir=\"ltr\">order</code>, <code dir=\"ltr\">flex-direction: row-reverse</code> or explicit grid placement, because focus will still follow the markup (2.4.3 Focus Order).</p>\n<p><code dir=\"ltr\">tabindex</code> adjusts focusability:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>Value</th>\n<th>Effect</th>\n<th>Use it for</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td><code dir=\"ltr\">0</code></td>\n<td>Focusable, in DOM order</td>\n<td>A custom control built from a non-interactive element</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">-1</code></td>\n<td>Focusable from script only, not with Tab</td>\n<td>Headings or containers you move focus to</td>\n</tr>\n<tr>\n<td><code dir=\"ltr\">1</code> or more</td>\n<td>Jumps ahead of everything else</td>\n<td>Never</td>\n</tr>\n</tbody>\n</table></div><aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">Common mistake</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Positive tabindex</span></p>\n<p>Ticketly had <code dir=\"ltr\">tabindex=&quot;1&quot;</code> on the Saturday button and <code dir=\"ltr\">tabindex=&quot;3&quot;</code> on Friday “to fix the order”. Positive values create a separate queue that is visited before the rest of the page, so focus now starts in the middle of the page and the order breaks whenever anyone adds a control. Remove every positive value and fix the DOM order instead.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Make focus visible</h2>\n<p>A keyboard user without a visible focus indicator is like a mouse user with an invisible pointer. WCAG 2.4.7 Focus Visible (AA) requires one; 1.4.11 Non-text Contrast means it should have at least <strong>3:1</strong> contrast against what surrounds it.</p>\n<p>The modern tool is <code dir=\"ltr\">:focus-visible</code>. Browsers apply it when focus should be shown: always for keyboard navigation and text fields, but usually not when a button is clicked with a mouse. That answers the designer’s real complaint without hurting anyone:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">ticketly/styles.css</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">:focus-visible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> #5b3cc4</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline-offset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Use <code dir=\"ltr\">outline</code> rather than <code dir=\"ltr\">box-shadow</code> for the ring. Windows contrast themes (forced colors mode) remove box shadows but keep outlines, so a shadow-only indicator vanishes for the people who most need it.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Skip links and sticky bars</h2>\n<p>Ticketly’s header has a logo, ten menu links, a search field and an account menu: thirteen Tab stops. A keyboard user has to tab through all of them on every page. WCAG 2.4.1 Bypass Blocks (A) asks for a way past repeated content, and the classic solution is a <strong>skip link</strong>: the first focusable element on the page, hidden until it receives focus.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">body</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">a</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> class</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"skip-link\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> href</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"#main\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Skip to main content&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">a</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">header</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">header</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  &#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"main\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">main</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">body</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.skip-link</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  position</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">absolute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inset-block-start</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  inset-inline-start</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  padding</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">8</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 12</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">#fff</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  transform</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">translateY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">-150</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.skip-link:focus</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  transform</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">none</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>Don’t hide it with <code dir=\"ltr\">display: none</code>, which would remove it from the tab order. Moving it off-screen and back on focus keeps it reachable and makes it appear exactly when a keyboard user needs it.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 200\" role=\"img\" aria-labelledby=\"l-a11y-3-1-t1\"><defs><marker id=\"arrow-l-a11y-3-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-a11y-3-1-t1\">Without a skip link, the first Tab press lands on the logo and twelve more presses cross the header before reaching main content. With a skip link, the first Tab shows Skip to main content, and Enter moves straight to main.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"30\">Without a skip link</text>\n  <rect class=\"d-box\" x=\"20\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"90\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"160\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <rect class=\"d-box\" x=\"230\" y=\"45\" width=\"60\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label-muted\" x=\"340\" y=\"67\" text-anchor=\"middle\">… 13 stops</text>\n  <rect class=\"d-box-primary\" x=\"400\" y=\"45\" width=\"140\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"470\" y=\"67\" text-anchor=\"middle\">main</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"130\">With a skip link</text>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"145\" width=\"200\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"120\" y=\"167\" text-anchor=\"middle\">Skip to main content</text>\n  <path class=\"d-arrow\" d=\"M220 162 L395 162\" marker-end=\"url(#arrow-l-a11y-3-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"305\" y=\"152\" text-anchor=\"middle\">Enter</text>\n  <rect class=\"d-box-primary\" x=\"400\" y=\"145\" width=\"140\" height=\"34\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"470\" y=\"167\" text-anchor=\"middle\">main</text>\n</svg><figcaption>The first Tab press on Ticketly</figcaption></figure>\n<p>Sticky headers and cookie banners bring the opposite problem: focus moves to an element that is hidden underneath them, which fails 2.4.11 Focus Not Obscured (AA). Tell the browser how much of the viewport the sticky parts cover, and it will scroll focused elements clear of them:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">html</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  scroll-padding-block-start</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">96</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">; </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">/* sticky header height plus a little air */</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>For a cookie banner, the real fix is not to let it float over content indefinitely: place it in the flow, or make sure it can be dismissed before anything else is reached.</p>\n<aside class=\"callout callout-tip\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">Tip</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>A two-minute keyboard check</span></p>\n<p>Tab from the address bar to the bottom of the page. At every stop, ask: can I see where I am, is this where I expected to be, and does Enter or Space do what a click would? Then Shift+Tab back up. You will find most keyboard bugs this way before any tool does.</p>\n</aside>\n<p>Ticketly can now be crossed from top to bottom with a keyboard. The next lesson handles the harder cases, where your code has to move focus itself: dialogs and single-page app navigation.</p>\n","toc":[{"id":"sec-1","text":"The keys people use","level":2},{"id":"sec-2","text":"Focus order follows the DOM","level":2},{"id":"sec-3","text":"Make focus visible","level":2},{"id":"sec-4","text":"Skip links and sticky bars","level":2}],"takeaways":["Focus order follows the DOM order, so put markup in the order people should meet it, and don’t fight it with CSS reordering.","Use tabindex=“0” to add a custom control to the tab order and tabindex=“-1” for elements you focus from script; never use a positive tabindex.","Never remove the focus outline without a replacement; style :focus-visible with an indicator of at least 3:1 contrast.","A skip link as the first focusable element lets keyboard users bypass repeated navigation, and scroll-padding stops sticky bars hiding focus."],"further":[{"title":"MDN: :focus-visible","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible"},{"title":"MDN: tabindex","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex"},{"title":"W3C: Understanding 2.4.7 Focus Visible","url":"https://www.w3.org/WAI/WCAG22/Understanding/focus-visible.html"},{"title":"W3C APG: Developing a Keyboard Interface","url":"https://www.w3.org/WAI/ARIA/apg/practices/keyboard-interface/"}],"quiz":[{"q":"<p>In what order does Tab visit these buttons?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">HTML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Friday&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"2\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Saturday&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> tabindex</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"1\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Sunday&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Weekend&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n","options":[{"text":"Friday, Saturday, Sunday, Weekend","why":"That is DOM order, which is what you get when no element has a positive tabindex. Positive values jump the queue."},{"text":"Sunday, Saturday, Friday, Weekend","why":"Correct. Elements with positive tabindex come first in ascending order, then everything else in DOM order. This is why positive values cause chaos."},{"text":"Saturday, Sunday, Friday, Weekend","why":"Positive tabindex values are visited in ascending order, so 1 (Sunday) comes before 2 (Saturday)."},{"text":"Friday, Weekend, Sunday, Saturday","why":"Positive tabindex elements are visited before the tabindex 0 elements, not after them."}],"answer":1},{"q":"<p>A designer finds the default focus ring ugly. Which CSS is the best response?</p>","options":[{"text":"<code dir=\"ltr\">:focus-visible { outline: 3px solid var(--brand); outline-offset: 2px; }</code>","why":"Correct. It replaces the default with a strong, on-brand indicator that keyboard users see, while mouse clicks on buttons don’t show it."},{"text":"<code dir=\"ltr\">*:focus { outline: none; }</code>","why":"This removes the indicator for everyone, and keyboard users lose track of where they are. It fails 2.4.7 Focus Visible."},{"text":"<code dir=\"ltr\">:focus { outline: 1px dotted #ddd; }</code>","why":"A thin light-grey line on a white page is nearly invisible. Indicators need at least 3:1 contrast against their surroundings."}],"answer":0},{"q":"<p>When should an element get <code dir=\"ltr\">tabindex=&quot;-1&quot;</code>?</p>","options":[{"text":"When you want to remove a button from the tab order because it is less important","why":"Every interactive control must be keyboard reachable. Removing a working button from the tab order makes it unusable for keyboard users."},{"text":"On decorative images so screen readers skip them","why":"tabindex only affects keyboard focus. Decorative images need alt=“” or aria-hidden, and they aren’t focusable anyway."},{"text":"On every element inside the main region, to keep focus order tidy","why":"That would remove links and buttons in main from the tab order, making the page impossible to use from the keyboard."},{"text":"On an element you need to focus from script, such as a heading after a route change, that shouldn’t be a Tab stop","why":"Correct. tabindex=“-1” makes an element focusable with element.focus() without adding it to the Tab sequence."}],"answer":3},{"q":"<p>Ticketly’s sticky header is 80px tall. When a keyboard user tabs backwards up the page, focused links slide underneath it. Which fix addresses 2.4.11 Focus Not Obscured?</p>","options":[{"text":"Give the header <code dir=\"ltr\">z-index: -1</code> when anything is focused","why":"Sending the header behind content would make it overlap visually in strange ways and doesn’t reliably keep focus in view."},{"text":"Add <code dir=\"ltr\">html { scroll-padding-block-start: 96px; }</code> so the browser scrolls focused elements clear of the header","why":"Correct. scroll-padding tells the browser which part of the viewport is covered, so scrolling into view leaves room for the sticky header."},{"text":"Add <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code> to the header","why":"aria-hidden only affects assistive technology. The header would still cover the focused link visually, and its links would vanish from the tree."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<header class=\"site-header\">\n  <a href=\"#\" class=\"logo\">Ticketly</a>\n  <nav aria-label=\"Site\">\n    <a href=\"#events\">Events</a>\n    <a href=\"#faq\">FAQ</a>\n    <a href=\"#account\">Account</a>\n  </nav>\n</header>\n<main id=\"main\">\n  <h1>Harbour Lights 2026</h1>\n  <div class=\"passes\">\n    <button tabindex=\"3\">Add Friday pass</button>\n    <button tabindex=\"1\">Add Saturday pass</button>\n    <button>Add weekend pass</button>\n  </div>\n</main>","css":".site-header { display: flex; gap: 16px; align-items: center; }\nnav { display: flex; gap: 12px; }\n.passes { display: flex; flex-wrap: wrap; gap: 8px; }\nbutton { font: inherit; padding: 8px 14px; border: 0; border-radius: 8px; background: #5b3cc4; color: #fff; }\n\n/* \"Remove the ugly blue box\" (2019) */\n*:focus { outline: none; }\n","js":""},"solution":{"html":"<a class=\"skip-link\" href=\"#main\">Skip to main content</a>\n<header class=\"site-header\">\n  <a href=\"#\" class=\"logo\">Ticketly</a>\n  <nav aria-label=\"Site\">\n    <a href=\"#events\">Events</a>\n    <a href=\"#faq\">FAQ</a>\n    <a href=\"#account\">Account</a>\n  </nav>\n</header>\n<main id=\"main\">\n  <h1>Harbour Lights 2026</h1>\n  <div class=\"passes\">\n    <button>Add Friday pass</button>\n    <button>Add Saturday pass</button>\n    <button>Add weekend pass</button>\n  </div>\n</main>","css":".site-header { display: flex; gap: 16px; align-items: center; }\nnav { display: flex; gap: 12px; }\n.passes { display: flex; flex-wrap: wrap; gap: 8px; }\nbutton { font: inherit; padding: 8px 14px; border: 0; border-radius: 8px; background: #5b3cc4; color: #fff; }\n\n:focus-visible {\n  outline: 3px solid #1b1830;\n  outline-offset: 2px;\n}\n\n.skip-link {\n  position: absolute;\n  inset-block-start: 8px;\n  inset-inline-start: 8px;\n  padding: 8px 12px;\n  background: #fff;\n  color: #1b1830;\n  transform: translateY(-150%);\n}\n.skip-link:focus {\n  transform: none;\n}\n","js":""},"checks":[{"id":"no-positive","test":"return $$('[tabindex]').every((el) => Number(el.getAttribute('tabindex')) <= 0);"},{"id":"skip-first","test":"const focusable = $$('a[href], button, input, select, textarea, [tabindex]').filter((el) => el.tabIndex >= 0 && !el.disabled && getComputedStyle(el).display !== 'none' && getComputedStyle(el).visibility !== 'hidden'); const first = focusable[0]; return !!first && first.tagName === 'A' && /skip/i.test(first.textContent) && first.getAttribute('href') === '#' + $('main').id;"},{"id":"skip-visible","test":"const link = $$('a').find((a) => /skip/i.test(a.textContent)); if (!link) return false; link.focus(); await tick(50); const r = link.getBoundingClientRect(); return document.activeElement === link && r.width > 1 && r.height > 1 && r.bottom > 0 && r.top < window.innerHeight;"},{"id":"focus-ring","test":"const ok = (el) => { el.focus(); const cs = getComputedStyle(el); const outline = cs.outlineStyle !== 'none' && parseFloat(cs.outlineWidth) >= 2; return outline; }; const items = [...$$('main button'), ...$$('nav a')]; const result = items.every(ok); document.activeElement.blur(); return result;"}],"title":"Make Ticketly keyboard friendly","prompt":"<p>Three keyboard problems on one small page:</p>\n<ol>\n<li>The pass buttons use positive <code dir=\"ltr\">tabindex</code> values, so focus order jumps around. Remove them; the DOM order is already right.</li>\n<li>A 2019 rule hides every focus indicator. Replace it with a visible <code dir=\"ltr\">:focus-visible</code> outline at least 2px wide.</li>\n<li>There is no way to bypass the header. Add a skip link as the <strong>first focusable element</strong>, with text containing “Skip” and <code dir=\"ltr\">href=&quot;#main&quot;</code>. It may be hidden off-screen, but it must be visible while focused (don’t use <code dir=\"ltr\">display: none</code>).</li>\n</ol>\n","hints":["Delete the tabindex attributes from the buttons entirely.","Replace *:focus { outline: none; } with :focus-visible { outline: 3px solid …; outline-offset: 2px; }","Put <a class=\"skip-link\" href=\"#main\">Skip to main content</a> before the header, move it up with transform: translateY(-150%), and reset the transform in .skip-link:focus."],"explanation":"<p>Without positive tabindex values, Tab follows the markup: skip link, logo, menu, then the three passes in the order they appear. The focus rule now adds an indicator instead of removing one, and <code dir=\"ltr\">:focus-visible</code> keeps it from flashing on mouse clicks. The skip link is the first Tab stop; it stays reachable because it is moved off-screen rather than removed, and it slides into view on focus so sighted keyboard users see it too. Pressing Enter on it moves focus to <code dir=\"ltr\">#main</code>, past the header.</p>\n","checkLabels":{"no-positive":"No element has a positive tabindex","skip-first":"The first focusable element is a skip link pointing to #main","skip-visible":"The skip link is visible on screen while it has focus","focus-ring":"Links and buttons show an outline at least 2px wide when focused"}},"words":819,"readMinutes":4}