{"id":"l-a11y-3-3","courseId":"web-accessibility","lang":"en","kind":"lesson","summary":"Apply ARIA only where HTML falls short, keep states such as aria-pressed and aria-expanded in sync, and announce dynamic updates with status and alert live regions.","html":"<p>Everything so far has used plain HTML, and that is deliberate. HTML elements come with roles, names, states and keyboard behaviour built in. But sometimes HTML runs out. There is no native element for a toggle button’s pressed state, for a set of tabs, or for “say this out loud when the basket changes”. That is where <strong>ARIA</strong> (Accessible Rich Internet Applications) comes in: a set of attributes that let you describe roles, states and relationships directly to the accessibility tree.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The first rule of ARIA</h2>\n<p>The W3C’s own guidance starts with a rule that surprises people: <strong>if you can use a native HTML element with the behaviour you need, do that instead of ARIA.</strong> WebAIM’s annual scans of the top million home pages keep finding that pages using ARIA have more detected errors, not fewer. The reason is simple. ARIA only changes what is <strong>announced</strong>. It doesn’t make anything focusable, doesn’t add keyboard handling, and doesn’t change appearance. Every ARIA attribute is a promise to assistive technology that your JavaScript then has to keep.</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:#6E7781;--shiki-dark:#8B949E\">&#x3C;!-- A promise with nothing behind it --></span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">&#x3C;</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</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:#1F2328;--shiki-dark:#E6EDF3\">>Add to basket&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">span</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">></span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">&#x3C;!-- The same promise, kept by the browser --></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:#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<h2 id=\"sec-2\" tabindex=\"-1\">Roles, properties and states</h2>\n<p>ARIA attributes fall into three groups.</p>\n<p><strong>Roles</strong> say what something is: <code dir=\"ltr\">role=&quot;tab&quot;</code>, <code dir=\"ltr\">role=&quot;dialog&quot;</code>, <code dir=\"ltr\">role=&quot;switch&quot;</code>, <code dir=\"ltr\">role=&quot;status&quot;</code>. You rarely need them for things HTML already has.</p>\n<p><strong>Properties</strong> describe relationships or fixed characteristics: <code dir=\"ltr\">aria-labelledby</code> and <code dir=\"ltr\">aria-describedby</code> (you used both on forms), <code dir=\"ltr\">aria-controls</code>, <code dir=\"ltr\">aria-required</code>, <code dir=\"ltr\">aria-haspopup</code>.</p>\n<p><strong>States</strong> describe things that change as the user interacts, and your script must update them: <code dir=\"ltr\">aria-expanded</code>, <code dir=\"ltr\">aria-pressed</code>, <code dir=\"ltr\">aria-checked</code>, <code dir=\"ltr\">aria-selected</code>, <code dir=\"ltr\">aria-current</code>, <code dir=\"ltr\">aria-disabled</code>, <code dir=\"ltr\">aria-invalid</code>.</p>\n<p>Ticketly’s wishlist heart is a good example. It is already a real button, but nothing tells a screen reader whether the event is saved. <code dir=\"ltr\">aria-pressed</code> turns it into a toggle 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\">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\">\"fav\"</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> aria-pressed</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"false\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">>Save to wishlist&#x3C;/</span><span style=\"color:#116329;--shiki-dark:#7EE787\">button</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\">ticketly/wishlist.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:#1F2328;--shiki-dark:#E6EDF3\">fav.</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\"> saved</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> fav.</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-pressed'</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\">  fav.</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-pressed'</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\">saved));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>NVDA now says “Save to wishlist, toggle button, not pressed”, then “pressed” after activation. Notice that the label stays the same; the state carries the change. And you can style from the state directly, which keeps the visuals and the semantics from drifting apart:</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:#0550AE;--shiki-dark:#79C0FF\">.fav</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">[</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">aria-pressed</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:#0550AE;--shiki-dark:#79C0FF\">  background</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </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\">  color</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:#1F2328;--shiki-dark:#E6EDF3\">}</span></span></code></pre></div>\n<p>For the current page in a navigation menu, <code dir=\"ltr\">aria-current=&quot;page&quot;</code> tells screen reader users which link they are on: “Tickets, current page, link”.</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>aria-hidden on something focusable</span></p>\n<p>A developer saw the “Add to basket” button announced twice (once in the card, once in a sticky bar) and added <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code> to one of them. The button is still in the tab order, so keyboard users land on it, and the screen reader says nothing because it is hidden from the tree. Never hide focusable elements. If something truly is a duplicate, remove it, or make it <code dir=\"ltr\">inert</code>.</p>\n</aside>\n<p>A few more rules from the W3C’s “Using ARIA” note keep you out of trouble:</p>\n<ul>\n<li>Don’t change native semantics unless you really must: <code dir=\"ltr\">&lt;h2 role=&quot;button&quot;&gt;</code> destroys the heading. Put a button inside the heading instead.</li>\n<li>Every interactive ARIA control must be keyboard operable.</li>\n<li>Every interactive element needs an accessible name.</li>\n</ul>\n<h2 id=\"sec-3\" tabindex=\"-1\">Live regions: announcing changes</h2>\n<p>When Ticketly adds a ticket, the basket counter in the header changes from 1 to 2. Sighted users notice. Screen reader users hear nothing, because their focus is still on the button they pressed. A <strong>live region</strong> is an element whose content changes are read out automatically, without moving focus.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"html\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">ticketly/index.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\">p</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\">\"basket-status\"</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\">\"status\"</span><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\">></span></span></code></pre></div>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"5\"><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\">function</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:#1F2328;--shiki-dark:#E6EDF3\">  count </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\">  basketStatus.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> `Ticket added. ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">count</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">} tickets in your basket.`</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<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 180\" role=\"img\" aria-labelledby=\"l-a11y-3-3-t1\"><defs><marker id=\"arrow-l-a11y-3-3-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-3-t1\">An empty status element exists from page load. When script changes its text, the browser fires a change event through the accessibility API, and the screen reader speaks the new text after the current speech, while focus stays on the button.</title>\n  <rect class=\"d-box\" x=\"20\" y=\"60\" width=\"170\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-code\" x=\"105\" y=\"88\" text-anchor=\"middle\">role=\"status\"</text>\n  <text class=\"d-label-muted\" x=\"105\" y=\"108\" text-anchor=\"middle\">empty, at load</text>\n  <path class=\"d-arrow\" d=\"M190 90 L260 90\" marker-end=\"url(#arrow-l-a11y-3-3-1)\"/>\n  <text class=\"d-code\" x=\"225\" y=\"78\" text-anchor=\"middle\">textContent</text>\n  <rect class=\"d-box-primary\" x=\"265\" y=\"60\" width=\"170\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"350\" y=\"88\" text-anchor=\"middle\">Browser sees</text>\n  <text class=\"d-label\" x=\"350\" y=\"108\" text-anchor=\"middle\">a change</text>\n  <path class=\"d-arrow\" d=\"M435 90 L500 90\" marker-end=\"url(#arrow-l-a11y-3-3-1)\"/>\n  <rect class=\"d-box-success\" x=\"505\" y=\"60\" width=\"160\" height=\"60\" rx=\"10\"/>\n  <text class=\"d-label\" x=\"585\" y=\"85\" text-anchor=\"middle\">\"Ticket added.</text>\n  <text class=\"d-label\" x=\"585\" y=\"105\" text-anchor=\"middle\">2 tickets…\"</text>\n  <text class=\"d-label-muted\" x=\"340\" y=\"160\" text-anchor=\"middle\">Focus never moves; the message is spoken when speech is idle</text>\n</svg><figcaption>How a live region announcement happens</figcaption></figure>\n<p>Three rules make live regions reliable:</p>\n<ol>\n<li><strong>The region must exist before the change.</strong> Screen readers watch existing live regions for changes. Inserting a new element that already contains text is often missed. Render the empty region at page load.</li>\n<li><strong>Pick the politeness.</strong> <code dir=\"ltr\">role=&quot;status&quot;</code> (same as <code dir=\"ltr\">aria-live=&quot;polite&quot;</code>) waits until the screen reader finishes its current sentence. <code dir=\"ltr\">role=&quot;alert&quot;</code> (assertive) interrupts. Reserve alerts for urgent, rare messages like a failed payment; an assertive “Ticket added” on every click is exhausting.</li>\n<li><strong>Keep messages short and meaningful.</strong> Announce the result (“2 tickets in your basket”), not the mechanics (“basket component updated”).</li>\n</ol>\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>When to move focus instead</span></p>\n<p>Live regions are for status updates while the user carries on. If the user must act on the new content, as with a dialog or the error in a form field, move focus to it instead. Moving focus and announcing the same message would make the screen reader say it twice.</p>\n</aside>\n<p>ARIA’s states and roles become most powerful in custom widgets, where no HTML element exists at all. The next lesson builds three of them following the ARIA Authoring Practices Guide.</p>\n","toc":[{"id":"sec-1","text":"The first rule of ARIA","level":2},{"id":"sec-2","text":"Roles, properties and states","level":2},{"id":"sec-3","text":"Live regions: announcing changes","level":2}],"takeaways":["The first rule of ARIA is to use a native HTML element when one exists; ARIA changes what is announced, never how anything behaves.","ARIA comes in roles (what it is), properties (relationships and fixed traits) and states (values that change, which your script must keep in sync).","Never put aria-hidden=“true” on a focusable element or override the role of a native element that already has the right one.","A live region must already be in the DOM before its text changes; use role=“status” for polite updates and role=“alert” only for urgent ones."],"further":[{"title":"W3C: Using ARIA (the rules of ARIA use)","url":"https://www.w3.org/TR/using-aria/"},{"title":"MDN: ARIA live regions","url":"https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Guides/Live_regions"},{"title":"W3C APG: Read Me First","url":"https://www.w3.org/WAI/ARIA/apg/practices/read-me-first/"}],"quiz":[{"q":"<p>What does adding <code dir=\"ltr\">role=&quot;button&quot;</code> to a <code dir=\"ltr\">&lt;span&gt;</code> change?</p>","options":[{"text":"It makes the span focusable and clickable with Enter and Space","why":"ARIA changes nothing about behaviour. You would still need tabindex=“0” and key handlers, which a real button gives you free."},{"text":"It changes the span’s styling to match the browser’s button style","why":"ARIA has no effect on styling. The span looks exactly the same."},{"text":"Only how assistive technology announces it, as “button”","why":"Correct. Roles change the accessibility tree and nothing else, which is why an ARIA button without keyboard support is a broken promise."}],"answer":2},{"q":"<p>Ticketly shows “Ticket added. 2 tickets in your basket.” in this element after a click. Why do screen readers stay silent?</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"js\" data-index=\"0\"><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\"> msg</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\">createElement</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'p'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">msg.</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\">'role'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'status'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">msg.textContent </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'Ticket added. 2 tickets in your basket.'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">basket.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">append</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(msg);</span></span></code></pre></div>\n","options":[{"text":"role=“status” is only announced for errors","why":"status is the polite live region role and is announced for any change; alert is the assertive one for urgent messages."},{"text":"Paragraph elements can’t be live regions","why":"Any element can be a live region. The timing is the problem, not the element type."},{"text":"It also needs aria-live=“assertive” to be announced at all","why":"Polite regions are announced too, after the user’s current speech finishes. Assertive only changes the priority."},{"text":"The live region was created together with its text, so there was no change for the screen reader to observe","why":"Correct. Put an empty role=“status” element in the page at load, then set its textContent when something happens."}],"answer":3},{"q":"<p>Which of these uses ARIA correctly?</p>","options":[{"text":"<code dir=\"ltr\">&lt;button aria-pressed=&quot;false&quot;&gt;Save to wishlist&lt;/button&gt;</code>, updated to “true” when saved","why":"Correct. A toggle button exposes its state with aria-pressed, and the script keeps it in sync, so users hear “Save to wishlist, toggle button, pressed”."},{"text":"<code dir=\"ltr\">&lt;h2 role=&quot;button&quot; onclick=&quot;toggle()&quot;&gt;Refunds&lt;/h2&gt;</code>","why":"This replaces the heading role, so it vanishes from heading navigation, and the “button” still has no keyboard support."},{"text":"<code dir=\"ltr\">&lt;a href=&quot;/faq&quot; aria-hidden=&quot;true&quot;&gt;FAQ&lt;/a&gt;</code>","why":"The link is still focusable but now has no name or role in the tree. Keyboard users land on a silent stop."},{"text":"<code dir=\"ltr\">&lt;nav role=&quot;navigation&quot;&gt;</code>","why":"Not harmful, but redundant. The nav element already has the navigation role, so the attribute adds nothing."}],"answer":0},{"q":"<p>The payment provider rejects a card at Ticketly’s checkout. Which live region role fits the message “Payment failed. Your card was declined.”?</p>","options":[{"text":"<code dir=\"ltr\">role=&quot;status&quot;</code>","why":"status waits politely until the screen reader finishes speaking. For a blocking failure the user must hear now, alert is the better fit."},{"text":"<code dir=\"ltr\">role=&quot;log&quot;</code>","why":"log is for sequences of messages such as chat history, where new entries are added at the end."},{"text":"<code dir=\"ltr\">role=&quot;alert&quot;</code>","why":"Correct. alert is assertive: it interrupts current speech, which is right for a rare, urgent message the user must act on."}],"answer":2}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<nav aria-label=\"Site\">\n  <a href=\"#home\">Home</a>\n  <a href=\"#tickets\" class=\"active\">Tickets</a>\n  <a href=\"#faq\">FAQ</a>\n</nav>\n\n<article class=\"pass\">\n  <h2>Friday pass</h2>\n  <button type=\"button\" id=\"add\">Add to basket</button>\n  <button type=\"button\" class=\"fav\" id=\"fav\">Save to wishlist</button>\n</article>\n\n<p id=\"basket-status\"></p>\n\n<div class=\"sticky-bar\">\n  <button type=\"button\" id=\"add-sticky\" aria-hidden=\"true\">Add to basket</button>\n</div>","css":"nav { display: flex; gap: 12px; margin-block-end: 12px; }\n.active { font-weight: 700; }\n.pass { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n.pass h2 { margin: 0; flex-basis: 100%; font-size: 1.2rem; }\nbutton { font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid #5b3cc4; background: #fff; color: #5b3cc4; }\n.fav.on, .fav[aria-pressed=\"true\"] { background: #5b3cc4; color: #fff; }\n.sticky-bar { margin-block-start: 16px; padding: 8px; background: #f1eefb; }\n","js":"let count = 0;\nconst status = document.querySelector('#basket-status');\nconst fav = document.querySelector('#fav');\n\nfunction addToBasket() {\n  count += 1;\n  status.textContent = `Ticket added. ${count} tickets in your basket.`;\n}\ndocument.querySelector('#add').addEventListener('click', addToBasket);\ndocument.querySelector('#add-sticky').addEventListener('click', addToBasket);\n\nfav.addEventListener('click', () => {\n  fav.classList.toggle('on');\n});\n"},"solution":{"html":"<nav aria-label=\"Site\">\n  <a href=\"#home\">Home</a>\n  <a href=\"#tickets\" class=\"active\" aria-current=\"page\">Tickets</a>\n  <a href=\"#faq\">FAQ</a>\n</nav>\n\n<article class=\"pass\">\n  <h2>Friday pass</h2>\n  <button type=\"button\" id=\"add\">Add to basket</button>\n  <button type=\"button\" class=\"fav\" id=\"fav\" aria-pressed=\"false\">Save to wishlist</button>\n</article>\n\n<p id=\"basket-status\" role=\"status\"></p>\n\n<div class=\"sticky-bar\">\n  <button type=\"button\" id=\"add-sticky\">Add Friday pass to basket</button>\n</div>","css":"nav { display: flex; gap: 12px; margin-block-end: 12px; }\n.active { font-weight: 700; }\n.pass { border: 1px solid #c9c4dc; border-radius: 12px; padding: 12px 16px; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n.pass h2 { margin: 0; flex-basis: 100%; font-size: 1.2rem; }\nbutton { font: inherit; padding: 6px 12px; border-radius: 8px; border: 1px solid #5b3cc4; background: #fff; color: #5b3cc4; }\n.fav.on, .fav[aria-pressed=\"true\"] { background: #5b3cc4; color: #fff; }\n.sticky-bar { margin-block-start: 16px; padding: 8px; background: #f1eefb; }\n","js":"let count = 0;\nconst status = document.querySelector('#basket-status');\nconst fav = document.querySelector('#fav');\n\nfunction addToBasket() {\n  count += 1;\n  status.textContent = `Ticket added. ${count} tickets in your basket.`;\n}\ndocument.querySelector('#add').addEventListener('click', addToBasket);\ndocument.querySelector('#add-sticky').addEventListener('click', addToBasket);\n\nfav.addEventListener('click', () => {\n  const saved = fav.getAttribute('aria-pressed') === 'true';\n  fav.setAttribute('aria-pressed', String(!saved));\n  fav.classList.toggle('on', !saved);\n});\n"},"checks":[{"id":"current-page","test":"const links = $$('nav a'); const cur = links.filter((a) => a.hasAttribute('aria-current')); return cur.length === 1 && cur[0].textContent.trim() === 'Tickets' && cur[0].getAttribute('aria-current') === 'page';"},{"id":"pressed-toggles","test":"const fav = $('#fav'); if (fav.tagName !== 'BUTTON' || fav.getAttribute('aria-pressed') !== 'false') return false; await click(fav); const on = fav.getAttribute('aria-pressed') === 'true'; await click(fav); return on && fav.getAttribute('aria-pressed') === 'false' && fav.textContent.trim() === 'Save to wishlist';"},{"id":"live-region","test":"const s = $('#basket-status'); const role = s.getAttribute('role'); const live = s.getAttribute('aria-live'); const isLive = role === 'status' || role === 'alert' || live === 'polite' || live === 'assertive'; if (!isLive) return false; await click('#add'); return /tickets? in your basket/.test(s.textContent);"},{"id":"polite","test":"const s = $('#basket-status'); return s.getAttribute('role') !== 'alert' && s.getAttribute('aria-live') !== 'assertive';"},{"id":"no-hidden-focusable","test":"const focusable = $$('a[href], button, input, select, textarea, [tabindex]').filter((el) => el.tabIndex >= 0 && !el.inert && !el.closest('[inert]')); return focusable.every((el) => !el.closest('[aria-hidden=\"true\"]')) && $$('button').length >= 3;"}],"title":"Add the ARIA HTML can't give you","prompt":"<p>Four small ARIA jobs on Ticketly’s pass card:</p>\n<ol>\n<li>Mark the <strong>Tickets</strong> link as the current page with <code dir=\"ltr\">aria-current=&quot;page&quot;</code>.</li>\n<li>Make <strong>Save to wishlist</strong> a toggle button: start with <code dir=\"ltr\">aria-pressed=&quot;false&quot;</code> and flip it on each click. Keep the label text unchanged.</li>\n<li>Make <code dir=\"ltr\">#basket-status</code> a <strong>polite</strong> live region so “Ticket added…” is announced.</li>\n<li>The sticky bar’s Add to basket button has <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code>, which leaves a silent Tab stop. Fix it without removing the button (you may change its text).</li>\n</ol>\n","hints":["aria-current and aria-pressed are plain attributes in the HTML; aria-pressed also needs updating in the click handler.","role=“status” is a polite live region. The element already exists at load, which is exactly what live regions need.","Remove aria-hidden from the sticky button. If the duplicate label bothers you, make it more specific, like “Add Friday pass to basket”."],"explanation":"<p><code dir=\"ltr\">aria-current=&quot;page&quot;</code> adds “current page” to the Tickets link announcement. The wishlist button keeps its label and exposes its state through <code dir=\"ltr\">aria-pressed</code>, which the click handler keeps in sync; the CSS can style from the same attribute. <code dir=\"ltr\">role=&quot;status&quot;</code> on the existing, empty paragraph means each new message is read politely without moving focus. And the sticky button is visible and focusable again in the accessibility tree: a focusable element must never sit inside <code dir=\"ltr\">aria-hidden=&quot;true&quot;</code>, because keyboard users reach it while screen readers stay silent.</p>\n","checkLabels":{"current-page":"Only the Tickets link has aria-current=“page”","pressed-toggles":"Save to wishlist starts at aria-pressed=“false” and toggles on each click","live-region":"#basket-status is a live region and announces the basket count","polite":"The basket message is polite, not an assertive alert","no-hidden-focusable":"No focusable element is hidden with aria-hidden"}},"words":729,"readMinutes":3}