{"id":"l-a11y-2-2","courseId":"web-accessibility","lang":"en","kind":"lesson","summary":"Choose correctly between a link and a button, and replace clickable divs and href=“#” links with native elements that work for keyboard and screen reader users.","html":"<p>Ticketly’s “Add to basket” control looks like a button: rounded corners, a brand colour, a pointer cursor on hover. It is a <code dir=\"ltr\">div</code> with an <code dir=\"ltr\">onclick</code>. Click it with a mouse and it works perfectly, which is exactly why nobody on the original team noticed. Press Tab and focus jumps straight past it. Samir’s screen reader reads it as plain text. A switch user can’t reach it at all.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Links go, buttons do</h2>\n<p>The decision between the two native controls comes down to one question: <strong>does it take the user somewhere?</strong></p>\n<ul>\n<li>A <strong>link</strong> (<code dir=\"ltr\">&lt;a href=&quot;…&quot;&gt;</code>) navigates: to another page, another part of this page (<code dir=\"ltr\">#lineup</code>), a file, an email address. Screen readers announce it as “link”. Users expect to open it in a new tab, copy its address, see it in history. It activates with <strong>Enter</strong>.</li>\n<li>A <strong>button</strong> (<code dir=\"ltr\">&lt;button&gt;</code>) performs an action on the current page: add to basket, open a dialog, submit a form, toggle a filter. Screen readers announce it as “button”. It activates with <strong>Enter and Space</strong>.</li>\n</ul>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 230\" role=\"img\" aria-labelledby=\"l-a11y-2-2-t1\"><defs><marker id=\"arrow-l-a11y-2-2-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-2-2-t1\">Decision flow: if activating the control changes the URL or moves to another place, use an a element with href; if it changes something on the current page, use a button element; never a div or span with a click handler.</title>\n  <rect class=\"d-box-primary\" x=\"200\" y=\"15\" width=\"280\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"340\" y=\"40\" text-anchor=\"middle\">Does it take the user</text>\n  <text class=\"d-label-strong\" x=\"340\" y=\"60\" text-anchor=\"middle\">to a new URL or place?</text>\n  <path class=\"d-arrow\" d=\"M260 71 L150 130\" marker-end=\"url(#arrow-l-a11y-2-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M420 71 L530 130\" marker-end=\"url(#arrow-l-a11y-2-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"180\" y=\"100\" text-anchor=\"middle\">yes</text>\n  <text class=\"d-label-muted\" x=\"500\" y=\"100\" text-anchor=\"middle\">no, it acts here</text>\n  <rect class=\"d-box-success\" x=\"40\" y=\"135\" width=\"220\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"150\" y=\"168\" text-anchor=\"middle\">&lt;a href=\"…\"&gt;</text>\n  <rect class=\"d-box-success\" x=\"420\" y=\"135\" width=\"220\" height=\"56\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"530\" y=\"168\" text-anchor=\"middle\">&lt;button&gt;</text>\n  <text class=\"d-label-muted\" x=\"340\" y=\"220\" text-anchor=\"middle\">Never: div or span with onclick</text>\n</svg><figcaption>Picking the right element</figcaption></figure>\n<p>Styling is irrelevant to the choice. A link can look like a big call-to-action and a button can look like a text link; CSS handles appearance, the element handles behaviour.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">What the clickable div is missing</h2>\n<p>To understand why the native element wins, count what a <code dir=\"ltr\">div</code> lacks. Here is the minimum to rebuild a button by hand:</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\">div</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\">\"btn\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"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\">\"0\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Add to basket&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">JavaScript</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:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> fake</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">querySelector</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'.btn'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">fake.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'click'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, addToBasket);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">fake.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keydown'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </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:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (event.key </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Enter'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addToBasket</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (event.key </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ' '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) event.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">preventDefault</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// stop the page scrolling</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">fake.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addEventListener</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'keyup'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">event</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </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:#CF222E;--shiki-dark:#FF7B72\">  if</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (event.key </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ' '</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addToBasket</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(); </span><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// native buttons fire on Space release</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>That is a role, focus, Enter, Space and scroll prevention, and it still lacks a disabled state, form submission, and the way Windows contrast themes (forced colors mode) style buttons. Compare:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"2\"><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:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</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\">\"btn\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onclick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addToBasket</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Add to basket&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>One element, and every behaviour is correct in every browser and every screen reader. This is the pattern you will see throughout the course: <strong>native HTML first</strong>, because the browser has already done the hard work.</p>\n<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>The href=“#” action link</span></p>\n<p><code dir=\"ltr\">&lt;a href=&quot;#&quot; onclick=&quot;removeItem()&quot;&gt;Remove&lt;/a&gt;</code> is focusable and works with Enter, so it looks fixed. But it is announced as a link, doesn’t respond to Space, and if the script fails it jumps to the top of the page and adds a history entry. An <code dir=\"ltr\">&lt;a&gt;</code> without any <code dir=\"ltr\">href</code> is worse: it isn’t even focusable. If it acts, make it a button.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Buttons in forms, and disabled buttons</h2>\n<p>Inside a <code dir=\"ltr\">&lt;form&gt;</code>, a <code dir=\"ltr\">&lt;button&gt;</code> defaults to <code dir=\"ltr\">type=&quot;submit&quot;</code>. Ticketly’s checkout has an “Apply promo code” button that kept submitting the whole form. The fix is one attribute:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"3\"><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:#0550AE;--shiki-dark:#79C0FF\"> type</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"button\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onclick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">applyPromo</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Apply promo code&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>The <code dir=\"ltr\">disabled</code> attribute removes a button from the tab order, so keyboard users tabbing through the form skip straight past it, and nobody is told why it is unavailable. A screen reader in browse mode still finds it, announced as “unavailable”, but with no reason attached. For a “Pay now” button that is waiting on a valid card, consider leaving it enabled and explaining the problem when it is pressed, which you will build in <a href=\"/lesson/web-accessibility/l-a11y-2-3\">Accessible Forms</a>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Text that makes sense alone</h2>\n<p>Screen reader users often pull up a list of all links or buttons on a page, and voice control users say the visible text to click it. “Read more”, “Click here” and “Details” are meaningless in that list. WCAG 2.4.4 asks that a link’s purpose be clear from its text, or its text plus its surrounding sentence or list item. Aim higher than the minimum: write “Friday line-up and set times”, not “Read more”. If the design insists on short visible text, you will learn in <a href=\"/lesson/web-accessibility/l-a11y-4-3\">Accessible Names and Descriptions</a> how to extend the name without breaking voice control.</p>\n<p>One more link habit: if a link opens a new tab or downloads a file, say so in its text, for example “Venue guide (PDF, 2 MB)” or “Seating plan (opens in a new tab)”. An unexpected new tab breaks the Back button for screen reader and magnifier users, who often don’t notice that the window changed and wonder why they can’t return.</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>The quick keyboard test</span></p>\n<p>Unplug your mouse (or ignore it) and press Tab through Ticketly. Every control you can click must receive focus, show where focus is, and respond to Enter, plus Space for buttons. Anything that fails is almost always a div, a span, or a link without an href.</p>\n</aside>\n<p>Real buttons and links fix the controls that act. Next come the controls that collect information, and the place where most inaccessible sites lose their customers: forms.</p>\n","toc":[{"id":"sec-1","text":"Links go, buttons do","level":2},{"id":"sec-2","text":"What the clickable div is missing","level":2},{"id":"sec-3","text":"Buttons in forms, and disabled buttons","level":2},{"id":"sec-4","text":"Text that makes sense alone","level":2}],"takeaways":["A link takes the user somewhere (a new URL or a fragment); a button does something on the current page.","Native buttons are focusable, announced as buttons, and activate with both Enter and Space, all for free.","A div with a click handler has no role, no focus and no keyboard support; rebuilding those by hand takes four separate fixes and is easy to get wrong.","Inside a form, give non-submitting buttons type=“button”, and write link and button text that makes sense out of context."],"further":[{"title":"MDN: The Button element","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/button"},{"title":"MDN: The Anchor element","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/a"},{"title":"W3C: Understanding 2.4.4 Link Purpose (In Context)","url":"https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context.html"}],"quiz":[{"q":"<p>Which of these should be a link rather than a button?</p>","options":[{"text":"“Add to basket”, which updates a counter on the page","why":"This changes state on the current page without navigating, so it is a button."},{"text":"“Show more dates”, which expands a hidden list","why":"Expanding content in place is an action, which makes it a button (a disclosure button, covered later in the course)."},{"text":"“Friday line-up”, which goes to <code dir=\"ltr\">/events/friday</code>","why":"Correct. It takes the user to a new URL, so it should be an a element with an href. Users can then open it in a new tab or copy it."}],"answer":2},{"q":"<p>A developer “fixes” this fake button. What is still missing?</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\">div</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"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\">\"0\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> onclick</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">addToBasket</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">()\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Add to basket&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n","options":[{"text":"Nothing; the role and tabindex make it equivalent to a button","why":"The role changes what is announced and tabindex adds focus, but neither makes the element respond to the keyboard."},{"text":"Keyboard activation; pressing Enter or Space does nothing because a div doesn’t fire click for keys","why":"Correct. You would need a keydown handler for Enter and Space (and to stop Space scrolling the page). A real button does this already."},{"text":"An aria-label, because screen readers can’t read text inside a div","why":"The text “Add to basket” already gives it an accessible name. Adding aria-label would duplicate it at best."}],"answer":1},{"q":"<p>Inside Ticketly’s checkout form, the “Apply promo code” button submits the whole form and shows “Card number required”. Why?</p>","options":[{"text":"A button inside a form defaults to type=“submit”","why":"Correct. Add <code dir=\"ltr\">type=&quot;button&quot;</code> to buttons that run JavaScript instead of submitting the form."},{"text":"The button needs an aria-controls attribute pointing at the field","why":"aria-controls only describes a relationship to assistive technology; it never changes what the button does."},{"text":"Buttons can’t be used inside forms, so it should be a link","why":"Buttons are the normal controls inside forms. A link would be the wrong role for an action."}],"answer":0},{"q":"<p>A screen reader user pulls up a list of links on Ticketly and hears “Read more, Read more, Read more”. What is the best fix?</p>","options":[{"text":"Add <code dir=\"ltr\">title=&quot;Read more about Friday&quot;</code> to each link","why":"title is not reliably announced and is invisible to keyboard and touch users, so it is a weak fix."},{"text":"Remove the links and rely on the headings instead","why":"The links are still useful. The problem is that their text doesn’t identify the destination."},{"text":"Make the link text say where it goes, such as “Friday line-up and set times”","why":"Correct. Descriptive link text works in a list of links, for voice control users, and for sighted users skimming the page."}],"answer":2}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<p>Basket: <span id=\"basket-count\">0</span> tickets</p>\n<article class=\"pass\">\n  <h2>Friday pass</h2>\n  <p>SAR 180, standing.</p>\n  <div class=\"btn\" onclick=\"addToBasket()\">Add to basket</div>\n  <a href=\"#\" class=\"text-btn\" onclick=\"removeFromBasket(); return false;\">Remove one</a>\n  <span class=\"link\" onclick=\"location.hash = '#lineup'\">See the Friday line-up</span>\n</article>\n<h2 id=\"lineup\">Friday line-up</h2>\n<p>Nour Quartet, The Saltmarsh Band.</p>","css":".pass { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; display: grid; gap: 8px; justify-items: start; }\n.pass h2 { margin: 0; font-size: 1.2rem; }\n.pass p { margin: 0; }\n.btn { background: #5b3cc4; color: #fff; border: 0; border-radius: 8px; padding: 8px 16px; font: inherit; cursor: pointer; }\n.text-btn { background: none; border: 0; padding: 0; font: inherit; color: #5b3cc4; text-decoration: underline; cursor: pointer; }\n.link { color: #5b3cc4; text-decoration: underline; cursor: pointer; }\n","js":"let count = 0;\nfunction addToBasket() {\n  count += 1;\n  document.querySelector('#basket-count').textContent = String(count);\n}\nfunction removeFromBasket() {\n  count = Math.max(0, count - 1);\n  document.querySelector('#basket-count').textContent = String(count);\n}\n"},"solution":{"html":"<p>Basket: <span id=\"basket-count\">0</span> tickets</p>\n<article class=\"pass\">\n  <h2>Friday pass</h2>\n  <p>SAR 180, standing.</p>\n  <button type=\"button\" class=\"btn\" onclick=\"addToBasket()\">Add to basket</button>\n  <button type=\"button\" class=\"text-btn\" onclick=\"removeFromBasket()\">Remove one</button>\n  <a href=\"#lineup\" class=\"link\">See the Friday line-up</a>\n</article>\n<h2 id=\"lineup\">Friday line-up</h2>\n<p>Nour Quartet, The Saltmarsh Band.</p>","css":".pass { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; display: grid; gap: 8px; justify-items: start; }\n.pass h2 { margin: 0; font-size: 1.2rem; }\n.pass p { margin: 0; }\n.btn { background: #5b3cc4; color: #fff; border: 0; border-radius: 8px; padding: 8px 16px; font: inherit; cursor: pointer; }\n.text-btn { background: none; border: 0; padding: 0; font: inherit; color: #5b3cc4; text-decoration: underline; cursor: pointer; }\n.link { color: #5b3cc4; text-decoration: underline; cursor: pointer; }\n","js":"let count = 0;\nfunction addToBasket() {\n  count += 1;\n  document.querySelector('#basket-count').textContent = String(count);\n}\nfunction removeFromBasket() {\n  count = Math.max(0, count - 1);\n  document.querySelector('#basket-count').textContent = String(count);\n}\n"},"checks":[{"id":"add-is-button","test":"const find = (t) => $$('body *').filter((el) => el.children.length === 0 || el.matches('button, a')).find((el) => el.textContent.trim() === t); const el = find('Add to basket'); return !!el && !!el.closest('button');"},{"id":"add-works","test":"const b = $$('button').find((x) => x.textContent.trim() === 'Add to basket'); if (!b) return false; const before = Number(text('#basket-count')); await click(b); return Number(text('#basket-count')) === before + 1;"},{"id":"remove-is-button","test":"const find = (t) => $$('body *').filter((el) => el.children.length === 0 || el.matches('button, a')).find((el) => el.textContent.trim() === t); const el = find('Remove one'); const b = el && el.closest('button'); if (!b) return false; const before = Number(text('#basket-count')); await click(b); return Number(text('#basket-count')) === Math.max(0, before - 1);"},{"id":"lineup-is-link","test":"const find = (t) => $$('body *').filter((el) => el.children.length === 0 || el.matches('button, a')).find((el) => el.textContent.trim() === t); const el = find('See the Friday line-up'); const a = el && el.closest('a[href]'); return !!a && a.getAttribute('href').endsWith('#lineup');"},{"id":"no-fakes","test":"return $$('div[onclick], span[onclick], a[href=\"#\"], a:not([href])').length === 0;"}],"title":"Replace Ticketly's fake controls","prompt":"<p>The Friday pass card has three controls, and none of them is the right element:</p>\n<ul>\n<li><strong>Add to basket</strong> is a <code dir=\"ltr\">div</code> with <code dir=\"ltr\">onclick</code>. It runs <code dir=\"ltr\">addToBasket()</code>.</li>\n<li><strong>Remove one</strong> is an <code dir=\"ltr\">&lt;a href=&quot;#&quot;&gt;</code> that runs <code dir=\"ltr\">removeFromBasket()</code>.</li>\n<li><strong>See the Friday line-up</strong> is a <code dir=\"ltr\">span</code> that changes the URL hash to <code dir=\"ltr\">#lineup</code>.</li>\n</ul>\n<p>Rebuild each with the correct native element, keeping the visible text exactly as it is. The two actions must still update the basket count. Keep the class names if you like, so the styling stays.</p>\n","hints":["Ask of each control: does it take the user somewhere, or does it do something here?","The two basket actions are buttons. Give them type=“button”.","Navigating to #lineup is what a link with href=“#lineup” does on its own; it needs no JavaScript."],"explanation":"<p>Add to basket and Remove one change state on the page, so they are <code dir=\"ltr\">&lt;button type=&quot;button&quot;&gt;</code> elements: focusable, announced as buttons, and activated by Enter and Space with no extra code. See the Friday line-up moves the user to another place, so it is <code dir=\"ltr\">&lt;a href=&quot;#lineup&quot;&gt;</code>, which also works without JavaScript, shows the destination on hover and can be copied. Notice the solution has less code than the starter: the <code dir=\"ltr\">return false</code> and the hash-setting script are gone, because the native elements already behave correctly.</p>\n","checkLabels":{"add-is-button":"“Add to basket” is a real button","add-works":"Clicking “Add to basket” adds one ticket to the basket count","remove-is-button":"“Remove one” is a real button that lowers the count","lineup-is-link":"“See the Friday line-up” is a link with href ending in #lineup","no-fakes":"No div or span click handlers, and no links with href=“#” or no href, remain"}},"words":733,"readMinutes":3}