{"id":"l-a11y-3-4","courseId":"web-accessibility","lang":"en","kind":"lesson","summary":"Build disclosure, tabs and menu button widgets that follow the ARIA Authoring Practices Guide, with the right roles, states and keyboard behaviour, and know when not to.","html":"<p>Ticketly’s line-up section has three tabs, Friday, Saturday and Sunday, built from styled buttons that swap panels on click. With a mouse it is fine. With a screen reader it is a row of three unrelated buttons, nothing says which day is showing, and the panels change silently. There is no <code dir=\"ltr\">&lt;tabs&gt;</code> element in HTML, so this is ARIA’s home ground.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The ARIA Authoring Practices Guide</h2>\n<p>The <strong>ARIA Authoring Practices Guide (APG)</strong>, published by the W3C, describes about thirty common widget patterns: accordion, combobox, dialog, disclosure, listbox, menu button, tabs, tree and more. For each one it lists the roles, states and properties, and the exact <strong>keyboard interaction</strong> users expect. Screen reader users learn these conventions across thousands of sites, so following them means your widget behaves the way they already know.</p>\n<p>Two cautions. The APG examples are demonstrations, not production components, so test them in your context. And the APG itself says to prefer native HTML where it exists. That rules out a surprising number of custom widgets.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Disclosure: show and hide</h2>\n<p>The simplest pattern is a button that shows and hides a region, such as Ticketly’s FAQ answers. If the design allows it, native <code dir=\"ltr\">&lt;details&gt;</code> and <code dir=\"ltr\">&lt;summary&gt;</code> give you the whole thing with no script:</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\">details</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\">summary</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>How do refunds work?&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">summary</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\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>You can get a full refund up to 14 days before the festival.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</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\">details</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span></code></pre></div>\n<p>When you need a custom version, the essential state is <code dir=\"ltr\">aria-expanded</code> on the button:</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\">h3</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\"> 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\"> aria-expanded</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"refunds\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>How do refunds work?&#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\">h3</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\">div</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\">\"refunds\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hidden</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\">p</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>You can get a full refund up to 14 days before the festival.&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">p</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\">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=\"2\"><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:#1F2328;--shiki-dark:#E6EDF3\">button.</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\">, () </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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> open</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-expanded'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'true'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  button.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-expanded'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">open));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  panel.hidden </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> open;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>The screen reader announces “How do refunds work?, button, collapsed”, then “expanded”. Wrapping the button in a heading keeps the FAQ questions in heading navigation.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">Tabs</h2>\n<p>The tabs pattern has more moving parts:</p>\n<ul>\n<li>A container with <code dir=\"ltr\">role=&quot;tablist&quot;</code> (and a name, such as <code dir=\"ltr\">aria-label=&quot;Festival days&quot;</code>).</li>\n<li>Each tab with <code dir=\"ltr\">role=&quot;tab&quot;</code>, <code dir=\"ltr\">aria-selected=&quot;true&quot;</code> or <code dir=\"ltr\">&quot;false&quot;</code>, and <code dir=\"ltr\">aria-controls</code> pointing at its panel.</li>\n<li>Each panel with <code dir=\"ltr\">role=&quot;tabpanel&quot;</code> and <code dir=\"ltr\">aria-labelledby</code> pointing back at its tab.</li>\n<li><strong>Keyboard:</strong> Left and Right arrows move between tabs (wrapping at the ends), Home and End jump to the first and last, and Tab moves out of the tablist into the panel.</li>\n</ul>\n<p>That last point uses a technique called <strong>roving tabindex</strong>. Only the selected tab has <code dir=\"ltr\">tabindex=&quot;0&quot;</code>; the others have <code dir=\"ltr\">tabindex=&quot;-1&quot;</code>. The whole tablist is one Tab stop, and the arrow-key handler moves focus and the <code dir=\"ltr\">0</code> between tabs.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 200\" role=\"img\" aria-labelledby=\"l-a11y-3-4-t1\"><defs><marker id=\"arrow-l-a11y-3-4-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-4-t1\">Three tabs: Friday with tabindex minus one, Saturday selected with tabindex zero, Sunday with tabindex minus one. Arrow keys move between tabs; the Tab key goes from the selected tab down into the Saturday panel.</title>\n  <rect class=\"d-box\" x=\"40\" y=\"30\" width=\"150\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"115\" y=\"52\" text-anchor=\"middle\">Friday</text>\n  <text class=\"d-code\" x=\"115\" y=\"72\" text-anchor=\"middle\">tabindex=-1</text>\n  <rect class=\"d-box-primary\" x=\"210\" y=\"30\" width=\"150\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label-strong\" x=\"285\" y=\"52\" text-anchor=\"middle\">Saturday</text>\n  <text class=\"d-code\" x=\"285\" y=\"72\" text-anchor=\"middle\">tabindex=0</text>\n  <rect class=\"d-box\" x=\"380\" y=\"30\" width=\"150\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"455\" y=\"52\" text-anchor=\"middle\">Sunday</text>\n  <text class=\"d-code\" x=\"455\" y=\"72\" text-anchor=\"middle\">tabindex=-1</text>\n  <path class=\"d-arrow\" d=\"M360 45 L378 45\" marker-end=\"url(#arrow-l-a11y-3-4-1)\"/>\n  <path class=\"d-arrow\" d=\"M210 45 L192 45\" marker-end=\"url(#arrow-l-a11y-3-4-1)\"/>\n  <text class=\"d-label-muted\" x=\"600\" y=\"60\" text-anchor=\"middle\">← → arrows</text>\n  <path class=\"d-arrow\" d=\"M285 80 L285 125\" marker-end=\"url(#arrow-l-a11y-3-4-1)\"/>\n  <text class=\"d-label-muted\" x=\"320\" y=\"108\">Tab</text>\n  <rect class=\"d-box-accent\" x=\"40\" y=\"130\" width=\"490\" height=\"50\" rx=\"8\"/>\n  <text class=\"d-label\" x=\"285\" y=\"160\" text-anchor=\"middle\">tabpanel \"Saturday\" (line-up, set times)</text>\n</svg><figcaption>Roving tabindex in Ticketly’s day tabs</figcaption></figure>\n<p>Here is Ticketly’s version:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">ticketly/lineup.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\">\"tablist\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-label</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"Festival days\"</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\"> 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\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab\"</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\">\"tab-fri\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"true\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-fri\"</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\"> 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\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab\"</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\">\"tab-sat\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-sat\"</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\">>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\"> 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\"> role</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab\"</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\">\"tab-sun\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-selected</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"panel-sun\"</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\">div</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\">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\">\"tabpanel\"</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\">\"panel-fri\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-labelledby</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-fri\"</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\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</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\">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\">\"tabpanel\"</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\">\"panel-sat\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-labelledby</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-sat\"</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\"> hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">div</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\">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\">\"tabpanel\"</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\">\"panel-sun\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-labelledby</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"tab-sun\"</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\"> hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#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=\"4\"><div class=\"code-head\"><span class=\"code-title\">ticketly/tabs.js</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\"> tabs</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> [</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\">querySelectorAll</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'[role=\"tab\"]'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)];</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">function</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">tab</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  for</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> of</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs) {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> on</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tab;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    t.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">setAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-selected'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">String</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(on));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    t.tabIndex </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> on </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">?</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> :</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    document.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getElementById</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(t.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">getAttribute</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'aria-controls'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)).hidden </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> !</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">on;</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\">  tab.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">focus</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>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">tabs.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">forEach</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((</span><span style=\"color:#953800;--shiki-dark:#FFA657\">tab</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tab.</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\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tab)));</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><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\">'[role=\"tablist\"]'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</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\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> i</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">indexOf</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event.target);</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> moves</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { ArrowRight: i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, ArrowLeft: i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, Home: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, End: tabs.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</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\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">!</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(event.key </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">in</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> moves) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">||</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">===</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">return</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><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>\n<span class=\"line\"><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">  select</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(tabs[(moves[event.key] </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">+</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">%</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tabs.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">length</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>This uses <strong>automatic activation</strong>: arrowing to a tab shows its panel immediately, which the APG recommends when panels display without delay. If a panel has to load data, use manual activation instead: arrows only move focus, and Enter or Space selects. The panels get <code dir=\"ltr\">tabindex=&quot;0&quot;</code> because they contain no focusable elements of their own; that gives Tab somewhere to land.</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>Tabs for navigation</span></p>\n<p>If each “tab” loads a different URL, it isn’t a tab widget; it is navigation that looks like tabs. Use links in a <code dir=\"ltr\">nav</code> with <code dir=\"ltr\">aria-current=&quot;page&quot;</code> on the current one. Users can then open pages in new tabs and the Back button works.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">Menu button, and when not to use it</h2>\n<p>The APG <strong>menu button</strong> pattern is a button with <code dir=\"ltr\">aria-haspopup=&quot;menu&quot;</code> and <code dir=\"ltr\">aria-expanded</code> that opens an element with <code dir=\"ltr\">role=&quot;menu&quot;</code> containing <code dir=\"ltr\">role=&quot;menuitem&quot;</code> children. Arrow keys move through the items, Escape closes the menu and returns focus to the button. It is right for application-style action menus, such as Ticketly’s “⋯” menu on a booking with “Download tickets”, “Transfer” and “Cancel booking”.</p>\n<p>It is wrong for website navigation. The <code dir=\"ltr\">menu</code> role tells screen readers to expect desktop-application behaviour: they switch interaction modes and announce items as menu items, not links. For a header “Events” dropdown full of links, use a <strong>disclosure</strong>: a button with <code dir=\"ltr\">aria-expanded</code> that reveals an ordinary list of links. Simpler code, and it behaves the way users of websites expect.</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>Popover helps with the visuals, not the semantics</span></p>\n<p>The HTML <code dir=\"ltr\">popover</code> attribute with <code dir=\"ltr\">popovertarget</code> gives you top-layer rendering and light dismiss for free. When a button opens the popover declaratively through <code dir=\"ltr\">popovertarget</code>, Chrome, Edge, Firefox and Safari expose that button’s expanded or collapsed state automatically, so don’t add <code dir=\"ltr\">aria-expanded</code> yourself. If your script opens the popover with <code dir=\"ltr\">showPopover()</code> instead, nothing is exposed and you must manage <code dir=\"ltr\">aria-expanded</code> by hand. Either way, popover doesn’t decide which pattern you are building: you still choose menu or disclosure semantics and add the matching keyboard support.</p>\n</aside>\n<p>Section 3 has made Ticketly operable. Section 4 turns to what people see and read, starting with colour and contrast.</p>\n","toc":[{"id":"sec-1","text":"The ARIA Authoring Practices Guide","level":2},{"id":"sec-2","text":"Disclosure: show and hide","level":2},{"id":"sec-3","text":"Tabs","level":2},{"id":"sec-4","text":"Menu button, and when not to use it","level":2}],"takeaways":["The ARIA Authoring Practices Guide (APG) documents the roles, states and keyboard behaviour users expect from each widget pattern.","A disclosure is a button with aria-expanded that shows and hides content; native details and summary give you one for free.","Tabs use tablist, tab and tabpanel roles, aria-selected, a roving tabindex so only the active tab is a Tab stop, and arrow keys to move between tabs.","Use role=“menu” only for application-style action menus; site navigation dropdowns are disclosures of ordinary links."],"further":[{"title":"W3C APG: Tabs Pattern","url":"https://www.w3.org/WAI/ARIA/apg/patterns/tabs/"},{"title":"W3C APG: Disclosure (Show/Hide) Pattern","url":"https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/"},{"title":"W3C APG: Menu Button Pattern","url":"https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/"},{"title":"MDN: The Details disclosure element","url":"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details"}],"quiz":[{"q":"<p>In a correctly built tab widget, what happens when a keyboard user presses Tab while focus is on the selected tab?</p>","options":[{"text":"Focus moves to the next tab in the tablist","why":"Moving between tabs is the arrow keys’ job. If Tab visited every tab, users would need many presses to get past the widget."},{"text":"Focus moves to the next tab and selects it","why":"Tab never changes the selection in the APG pattern; arrow keys do."},{"text":"Nothing, because Tab is captured by the tablist","why":"Capturing Tab would create a keyboard trap. The widget must let Tab leave normally."},{"text":"Focus leaves the tablist and moves into the active tab panel (or the next focusable element in it)","why":"Correct. With a roving tabindex only the selected tab is in the Tab sequence, so the next Tab goes to the panel content."}],"answer":3},{"q":"<p>What is wrong with this FAQ toggle?</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:#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\"> aria-controls</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"refund-answer\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>How do refunds work?&#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\">div</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\">\"refund-answer\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> hidden</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>…&#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":"It should be a link because it reveals content","why":"Revealing content in place is an action, not navigation, so a button is correct."},{"text":"It needs aria-expanded, updated on every toggle, so users know whether the answer is open","why":"Correct. aria-expanded is what makes screen readers say “collapsed” or “expanded”. aria-controls on its own is barely announced."},{"text":"It needs role=“menu” because it opens something","why":"menu is for lists of application actions. A show and hide toggle is the disclosure pattern."}],"answer":1},{"q":"<p>Ticketly’s header has an “Events” dropdown containing six links to event pages. Which pattern fits best?</p>","options":[{"text":"A disclosure button with aria-expanded that shows a list of ordinary links","why":"Correct. Navigation dropdowns are disclosures. Users Tab through the links as normal, and screen readers announce them as links."},{"text":"A menu button with role=“menu” and role=“menuitem” on each link","why":"The menu role makes screen readers switch to application-style interaction and expect arrow-key navigation, which surprises users on website navigation."},{"text":"A tablist where each event is a tab","why":"Tabs switch between panels of content on the same page. These are links to other pages."}],"answer":0},{"q":"<p>Why does the APG tabs pattern give inactive tabs <code dir=\"ltr\">tabindex=&quot;-1&quot;</code>?</p>","options":[{"text":"To hide inactive tabs from screen readers","why":"tabindex doesn’t affect the accessibility tree. Inactive tabs are still announced, for example as “Saturday, tab, 2 of 3”."},{"text":"So the whole tablist is a single Tab stop, while arrow-key handlers can still focus each tab from script","why":"Correct. This is the roving tabindex technique: the active tab has tabindex=“0”, the rest “-1”, and the arrow keys move focus and the 0 between them."},{"text":"Because buttons inside a tablist can’t be focused otherwise","why":"Buttons are focusable by default. The -1 is there to take them out of the Tab sequence, not to make them focusable."}],"answer":1}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<h2>Line-up</h2>\n<div class=\"tabs\">\n  <button type=\"button\" class=\"tab active\" id=\"tab-fri\" data-panel=\"panel-fri\">Friday</button>\n  <button type=\"button\" class=\"tab\" id=\"tab-sat\" data-panel=\"panel-sat\">Saturday</button>\n  <button type=\"button\" class=\"tab\" id=\"tab-sun\" data-panel=\"panel-sun\">Sunday</button>\n</div>\n<div class=\"panel\" id=\"panel-fri\">\n  <p>Nour Quartet, 8 pm. The Saltmarsh Band, 10 pm.</p>\n</div>\n<div class=\"panel\" id=\"panel-sat\" hidden>\n  <p>Layla &amp; the Tides, 7 pm. Harbour Brass, 9 pm.</p>\n</div>\n<div class=\"panel\" id=\"panel-sun\" hidden>\n  <p>Family matinee, 3 pm. Closing set by DJ Marwa, 8 pm.</p>\n</div>","css":".tabs { display: flex; gap: 4px; border-block-end: 2px solid #5b3cc4; }\n.tab { font: inherit; padding: 8px 16px; border: 1px solid #c9c4dc; border-block-end: 0; border-radius: 8px 8px 0 0; background: #f4f2fb; color: #1b1830; }\n.tab.active, .tab[aria-selected=\"true\"] { background: #5b3cc4; color: #fff; }\n.panel { padding: 12px 4px; }\n:focus-visible { outline: 3px solid #1b1830; outline-offset: 2px; }\n","js":"const tabs = [...document.querySelectorAll('.tab')];\n\ntabs.forEach((tab) => {\n  tab.addEventListener('click', () => {\n    for (const t of tabs) {\n      const on = t === tab;\n      t.classList.toggle('active', on);\n      document.getElementById(t.dataset.panel).hidden = !on;\n    }\n  });\n});\n"},"solution":{"html":"<h2>Line-up</h2>\n<div class=\"tabs\" role=\"tablist\" aria-label=\"Festival days\">\n  <button type=\"button\" class=\"tab active\" role=\"tab\" id=\"tab-fri\" aria-selected=\"true\" aria-controls=\"panel-fri\">Friday</button>\n  <button type=\"button\" class=\"tab\" role=\"tab\" id=\"tab-sat\" aria-selected=\"false\" aria-controls=\"panel-sat\" tabindex=\"-1\">Saturday</button>\n  <button type=\"button\" class=\"tab\" role=\"tab\" id=\"tab-sun\" aria-selected=\"false\" aria-controls=\"panel-sun\" tabindex=\"-1\">Sunday</button>\n</div>\n<div class=\"panel\" role=\"tabpanel\" id=\"panel-fri\" aria-labelledby=\"tab-fri\" tabindex=\"0\">\n  <p>Nour Quartet, 8 pm. The Saltmarsh Band, 10 pm.</p>\n</div>\n<div class=\"panel\" role=\"tabpanel\" id=\"panel-sat\" aria-labelledby=\"tab-sat\" tabindex=\"0\" hidden>\n  <p>Layla &amp; the Tides, 7 pm. Harbour Brass, 9 pm.</p>\n</div>\n<div class=\"panel\" role=\"tabpanel\" id=\"panel-sun\" aria-labelledby=\"tab-sun\" tabindex=\"0\" hidden>\n  <p>Family matinee, 3 pm. Closing set by DJ Marwa, 8 pm.</p>\n</div>","css":".tabs { display: flex; gap: 4px; border-block-end: 2px solid #5b3cc4; }\n.tab { font: inherit; padding: 8px 16px; border: 1px solid #c9c4dc; border-block-end: 0; border-radius: 8px 8px 0 0; background: #f4f2fb; color: #1b1830; }\n.tab.active, .tab[aria-selected=\"true\"] { background: #5b3cc4; color: #fff; }\n.panel { padding: 12px 4px; }\n:focus-visible { outline: 3px solid #1b1830; outline-offset: 2px; }\n","js":"const tabs = [...document.querySelectorAll('[role=\"tab\"]')];\n\nfunction select(tab) {\n  for (const t of tabs) {\n    const on = t === tab;\n    t.classList.toggle('active', on);\n    t.setAttribute('aria-selected', String(on));\n    t.tabIndex = on ? 0 : -1;\n    document.getElementById(t.getAttribute('aria-controls')).hidden = !on;\n  }\n  tab.focus();\n}\n\ntabs.forEach((tab) => tab.addEventListener('click', () => select(tab)));\n\ndocument.querySelector('[role=\"tablist\"]').addEventListener('keydown', (event) => {\n  const i = tabs.indexOf(event.target);\n  const moves = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tabs.length - 1 };\n  if (i === -1 || !(event.key in moves)) return;\n  event.preventDefault();\n  select(tabs[(moves[event.key] + tabs.length) % tabs.length]);\n});\n"},"checks":[{"id":"roles","test":"const list = $$('[role=\"tablist\"]'); if (list.length !== 1) return false; const tabs = $$('[role=\"tab\"]'); return tabs.length === 3 && tabs.every((t) => list[0].contains(t)) && $$('[role=\"tabpanel\"]').length === 3;"},{"id":"linked","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); return tabs.length === 3 && tabs.every((t) => { const p = panelOf(t); return p && p.getAttribute('role') === 'tabpanel' && (p.getAttribute('aria-labelledby') || '').split(/\\s+/).includes(t.id); });"},{"id":"initial-state","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); return tabs.length === 3 && selectedIs('Friday');"},{"id":"roving","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); const sel = tabs.find((t) => t.getAttribute('aria-selected') === 'true'); return tabs.length === 3 && !!sel && tabs.every((t) => t.tabIndex === (t === sel ? 0 : -1));"},{"id":"click-selects","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); if (tabs.length !== 3) return false; await click(byName('Saturday')); return selectedIs('Saturday') && byName('Saturday').tabIndex === 0 && byName('Friday').tabIndex === -1;"},{"id":"arrow-keys","test":"const tabs = $$('[role=\"tab\"]'); const byName = (n) => tabs.find((t) => t.textContent.trim() === n); const panelOf = (t) => document.getElementById(t.getAttribute('aria-controls')); const shown = (p) => !!p && !p.hidden && getComputedStyle(p).display !== 'none'; const selectedIs = (n) => tabs.every((t) => t.getAttribute('aria-selected') === String(t === byName(n))) && tabs.every((t) => shown(panelOf(t)) === (t === byName(n))); if (tabs.length !== 3) return false; await press(byName('Saturday'), 'ArrowRight'); if (!selectedIs('Sunday') || document.activeElement !== byName('Sunday')) return false; await press(byName('Sunday'), 'ArrowRight'); if (!selectedIs('Friday') || document.activeElement !== byName('Friday')) return false; await press(byName('Friday'), 'ArrowLeft'); return selectedIs('Sunday') && document.activeElement === byName('Sunday');"}],"title":"Build accessible day tabs","prompt":"<p>Ticketly’s line-up tabs work only with a mouse and say nothing to screen readers. Turn them into the APG tabs pattern:</p>\n<ul>\n<li><code dir=\"ltr\">role=&quot;tablist&quot;</code> on the container (name it, for example <code dir=\"ltr\">aria-label=&quot;Festival days&quot;</code>), <code dir=\"ltr\">role=&quot;tab&quot;</code> on each button, <code dir=\"ltr\">role=&quot;tabpanel&quot;</code> on each panel.</li>\n<li>Each tab has <code dir=\"ltr\">aria-controls</code> pointing to its panel; each panel has <code dir=\"ltr\">aria-labelledby</code> pointing back to its tab.</li>\n<li><code dir=\"ltr\">aria-selected</code> is <code dir=\"ltr\">&quot;true&quot;</code> on the selected tab and <code dir=\"ltr\">&quot;false&quot;</code> on the others. Friday starts selected.</li>\n<li>Roving tabindex: the selected tab has <code dir=\"ltr\">tabindex=&quot;0&quot;</code>, the others <code dir=\"ltr\">-1</code>.</li>\n<li>Clicking a tab selects it. <strong>ArrowRight</strong> and <strong>ArrowLeft</strong> select and focus the next or previous tab, wrapping around at the ends.</li>\n</ul>\n","hints":["Keep the tab text exactly Friday, Saturday and Sunday; the checks find tabs by their text.","Write one select(tab) function that updates aria-selected, tabIndex and the panels’ hidden attribute, then calls tab.focus().","In a keydown listener on the tablist, find the index of event.target in the tabs array, add or subtract 1, and wrap with (index + tabs.length) % tabs.length."],"explanation":"<p>The roles turn three loose buttons into one widget that screen readers announce as “Festival days, tab list, Friday, tab, selected, 1 of 3”. <code dir=\"ltr\">aria-controls</code> and <code dir=\"ltr\">aria-labelledby</code> link each tab and panel both ways. Because only the selected tab has <code dir=\"ltr\">tabindex=&quot;0&quot;</code>, the tablist is a single Tab stop; the arrow-key handler moves the selection, the focus and the <code dir=\"ltr\">0</code> together, and wraps at the ends as the APG describes. Having one <code dir=\"ltr\">select()</code> function keep every state in sync is what makes the widget trustworthy: the visuals (<code dir=\"ltr\">.active</code>), the semantics (<code dir=\"ltr\">aria-selected</code>) and the focus can never disagree.</p>\n","checkLabels":{"roles":"One tablist holds three tabs, and there are three tabpanels","linked":"Each tab controls its panel, and each panel is labelled by its tab","initial-state":"Friday starts selected and only its panel is visible","roving":"Only the selected tab has tabindex 0; the others have -1","click-selects":"Clicking Saturday selects it, shows its panel and moves the tabindex","arrow-keys":"Arrow keys select and focus the next or previous tab, wrapping at the ends"}},"words":737,"readMinutes":4}