{"id":"l-ds-3-3","courseId":"design-systems","lang":"en","kind":"lesson","summary":"Build every interaction state and accessibility behavior into components by default, including visible focus with :focus-visible, disabled and pressed states, and ARIA patterns that follow the WAI-ARIA Authoring Practices.","html":"<p>Northwind’s 2021 Button looked perfect in every screenshot, because screenshots only show the default state. In production it had no visible keyboard focus, its disabled style failed contrast on the Tidewater brand, and its “loading” state made the button shrink and the layout jump. None of that was in the Figma file. Components fail in their states, so the system has to design, build and test all of them.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">The state matrix</h2>\n<p>Every interactive component gets a state matrix: one row per variant, one column per state. For Button:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"Scrollable table\" tabindex=\"0\"><table><thead>\n<tr>\n<th>State</th>\n<th>Trigger</th>\n<th>Built with</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>Hover</td>\n<td>Pointer over it</td>\n<td><code dir=\"ltr\">:hover</code></td>\n</tr>\n<tr>\n<td>Focus visible</td>\n<td>Keyboard focus</td>\n<td><code dir=\"ltr\">:focus-visible</code></td>\n</tr>\n<tr>\n<td>Active</td>\n<td>Being pressed</td>\n<td><code dir=\"ltr\">:active</code></td>\n</tr>\n<tr>\n<td>Disabled</td>\n<td>Not available</td>\n<td><code dir=\"ltr\">disabled</code> or <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code></td>\n</tr>\n<tr>\n<td>Loading</td>\n<td>Action in progress</td>\n<td><code dir=\"ltr\">aria-busy</code>, stable width</td>\n</tr>\n<tr>\n<td>Pressed</td>\n<td>Toggle is on</td>\n<td><code dir=\"ltr\">aria-pressed=&quot;true&quot;</code></td>\n</tr>\n</tbody>\n</table></div><p>Northwind designs every cell in Figma (as component variants) before anyone writes code, and the visual tests in Section 4 render every cell in every theme. When a contribution arrives with only default and hover designed, the review sends it back.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">Focus that people can see</h2>\n<p>Keyboard users need to see where focus is; WCAG 2.2 requires it at level AA (2.4.7, Focus Visible). Mouse users usually don’t want a ring after clicking. <code dir=\"ltr\">:focus-visible</code> resolves this: browsers apply it when focus should be visible, typically after keyboard navigation, and not after most mouse clicks.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"0\"><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\">:root</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-focus-ring-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-blue-600</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-focus-ring-width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-focus-ring-offset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">px</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">}</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.nw-button:focus-visible</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-focus-ring-width</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">solid</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-focus-ring-color</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  outline-offset</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-focus-ring-offset</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>The ring is built from tokens, so dark mode and Tidewater can adjust it, and every component uses the same ring. It is an <code dir=\"ltr\">outline</code>, not a <code dir=\"ltr\">box-shadow</code>, because forced-colors modes like Windows contrast themes remove box shadows but keep outlines.</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>outline: none on :focus</span></p>\n<p><code dir=\"ltr\">:focus { outline: none; }</code> is still the most common accessibility bug in component libraries, usually added because the ring looked wrong on click. It removes the browser’s default ring for everyone, including keyboard users. If you remove a default, replace it in <code dir=\"ltr\">:focus-visible</code> in the same commit.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">Disabled, two ways</h2>\n<p>The <code dir=\"ltr\">disabled</code> attribute removes a button from the tab order and from activation. That’s right for most cases, but it creates a problem: keyboard and screen reader users can’t reach the button to learn <em>why</em> it is disabled.</p>\n<p>When you need an explanation, use <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code> instead. The button stays focusable and is announced as unavailable; you connect the reason with <code dir=\"ltr\">aria-describedby</code>; and because the browser won’t block the click for you, the component must:</p>\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:#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:#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\"> (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-disabled'</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\">    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:#1F2328;--shiki-dark:#E6EDF3\">    event.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">stopImmediatePropagation</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span></code></pre></div>\n<p>The system’s Button does this internally, so teams set one prop and get the behavior. That’s the whole point of building accessibility in: forty teams won’t each remember <code dir=\"ltr\">stopImmediatePropagation</code>, but one component can.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">Toggles, targets and patterns</h2>\n<p>A button that switches something on and off is a <strong>toggle button</strong>. Mark its state with <code dir=\"ltr\">aria-pressed=&quot;true&quot;</code> or <code dir=\"ltr\">&quot;false&quot;</code>, and style that attribute so the visual and announced states can’t drift apart:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"css\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">CSS</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"Copy code\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>Copy</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.nw-button</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:#953800;--shiki-dark:#FFA657\">  --nw-button-bg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-color-action-bg-subtle</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">  --nw-button-border</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">var</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#953800;--shiki-dark:#FFA657\">--nw-color-action-bg</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>Size matters too: WCAG 2.2 success criterion 2.5.8 asks for pointer targets of at least 24 by 24 CSS pixels at level AA, with some exceptions. Northwind’s smallest control height is 32px, enforced by the <code dir=\"ltr\">sm</code> size tokens.</p>\n<p>For anything beyond a button, use native elements first: <code dir=\"ltr\">&lt;button&gt;</code>, <code dir=\"ltr\">&lt;dialog&gt;</code>, <code dir=\"ltr\">&lt;details&gt;</code>, <code dir=\"ltr\">&lt;select&gt;</code>. When you build a custom widget such as tabs, a menu button or a combobox, follow the matching pattern in the WAI-ARIA Authoring Practices Guide: its roles, states and keyboard interactions. Users of assistive technology expect those behaviors, and the guide documents them precisely.</p>\n<aside class=\"callout callout-why\" 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\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">Why this matters</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>Why this matters</span></p>\n<p>At Northwind, an accessibility audit of a product built on the system found 31 issues. Twenty-six were in one-off components the team had built themselves; five were in system components, and fixing those five in the system fixed them in every product at once. Accessibility built into a shared component scales; accessibility left to each team’s memory doesn’t.</p>\n</aside>\n<p>In the exercise, you fix a button set that hides keyboard focus and ignores its own disabled state. Next, you write the documentation that tells teams how to use all of this.</p>\n","toc":[{"id":"sec-1","text":"The state matrix","level":2},{"id":"sec-2","text":"Focus that people can see","level":2},{"id":"sec-3","text":"Disabled, two ways","level":2},{"id":"sec-4","text":"Toggles, targets and patterns","level":2}],"takeaways":["Design and build a full state matrix for every interactive component, including focus, disabled, loading and pressed, not just default and hover.","Style keyboard focus with <code dir=\"ltr\">:focus-visible</code> and an outline from a token; never remove the outline without a visible replacement.","Use native elements first and follow the WAI-ARIA Authoring Practices for custom widgets like tabs, menus and comboboxes.","<code dir=\"ltr\">aria-disabled=&quot;true&quot;</code> keeps a control focusable so you can explain why it is unavailable, but you must block its action yourself.","When accessibility lives in the component, 40 teams get it for free; when it lives in the docs, most of them miss it."],"further":[{"title":":focus-visible (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:focus-visible"},{"title":"WAI-ARIA Authoring Practices Guide patterns (W3C)","url":"https://www.w3.org/WAI/ARIA/apg/patterns/"},{"title":"Understanding SC 2.5.8 Target Size (Minimum) (W3C)","url":"https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html"},{"title":"aria-pressed (MDN)","url":"https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-pressed"}],"quiz":[{"q":"<p>A team ships <code dir=\"ltr\">.nw-button:focus { outline: none; }</code> because the ring “looks ugly on click”. What’s the right fix in the system?</p>","options":[{"text":"Keep <code dir=\"ltr\">outline: none</code> and rely on the hover color for keyboard users.","why":"Keyboard users don’t hover, so they lose any indication of where they are."},{"text":"Move the ring to <code dir=\"ltr\">:focus-visible</code> so it shows for keyboard focus but not for most mouse clicks.","why":"Correct. Browsers apply <code dir=\"ltr\">:focus-visible</code> when focus needs to be visible, which is what both groups actually want."},{"text":"Replace the outline with a <code dir=\"ltr\">box-shadow</code> ring on <code dir=\"ltr\">:focus</code>.","why":"It still shows on click, and box shadows are removed in forced-colors modes, so high-contrast users lose the ring."},{"text":"Add <code dir=\"ltr\">tabindex=&quot;-1&quot;</code> to buttons so they can’t get focus.","why":"That removes buttons from the keyboard order entirely, making them unusable without a mouse."}],"answer":1},{"q":"<p>The Save button is unavailable until a driver is selected. Product wants a tooltip explaining why. Which approach works?</p>","options":[{"text":"Use the <code dir=\"ltr\">disabled</code> attribute and attach the tooltip to the button.","why":"A disabled button can’t receive focus, so keyboard and screen reader users never reach the tooltip."},{"text":"Hide the button until a driver is selected.","why":"Hiding it removes the hint that saving is possible at all, and the layout jumps when it appears."},{"text":"Set <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code>, keep it focusable, block the click in code, and connect the explanation with <code dir=\"ltr\">aria-describedby</code>.","why":"Correct. The control stays reachable and announced as unavailable, and the reason is available to everyone."}],"answer":2},{"q":"<p>Northwind needs a “Show only late shipments” toggle button. Which markup communicates its state correctly?</p>","options":[{"text":"<code dir=\"ltr\">&lt;button type=&quot;button&quot; aria-pressed=&quot;true&quot;&gt;Show only late shipments&lt;/button&gt;</code>","why":"Correct. <code dir=\"ltr\">aria-pressed</code> turns a button into a toggle, and screen readers announce it as pressed or not pressed."},{"text":"<code dir=\"ltr\">&lt;button type=&quot;button&quot; class=&quot;is-active&quot;&gt;Show only late shipments&lt;/button&gt;</code>","why":"The class changes the look but tells assistive technology nothing about the state."},{"text":"<code dir=\"ltr\">&lt;div role=&quot;button&quot; data-on=&quot;true&quot;&gt;Show only late shipments&lt;/div&gt;</code>","why":"A div needs keyboard handling and focusability added by hand, and <code dir=\"ltr\">data-on</code> isn’t exposed to assistive technology."},{"text":"<code dir=\"ltr\">&lt;button type=&quot;button&quot; aria-label=&quot;on&quot;&gt;Show only late shipments&lt;/button&gt;</code>","why":"The label replaces the visible name with “on”, which is worse than saying nothing."}],"answer":0},{"q":"<p>Why does Northwind draw focus rings with <code dir=\"ltr\">outline</code> rather than <code dir=\"ltr\">box-shadow</code>?</p>","options":[{"text":"Outlines are faster to render.","why":"Rendering cost isn’t a meaningful difference here."},{"text":"Box shadows can’t use custom properties.","why":"They can; any CSS value can come from a custom property."},{"text":"Outlines always render outside the border box, so offsets are impossible with shadows.","why":"Both can create gaps; <code dir=\"ltr\">outline-offset</code> is just more convenient. The deciding reason is elsewhere."},{"text":"Forced-colors modes such as Windows contrast themes remove box shadows but keep outlines.","why":"Correct. An outline-based ring survives high-contrast settings, so those users still see focus."}],"answer":3}],"exercise":{"kind":"web","mode":"dom","starter":{"html":"<div class=\"toolbar\">\n  <button class=\"nw-button\" type=\"button\" id=\"assign\">Assign driver</button>\n  <button class=\"nw-button\" type=\"button\" id=\"late\" aria-pressed=\"false\">Show only late</button>\n  <button class=\"nw-button\" type=\"button\" id=\"save\" aria-disabled=\"true\" aria-describedby=\"save-why\">Save route</button>\n  <p id=\"save-why\">Select a driver before saving.</p>\n</div>\n<p id=\"log\">Saved 0 times</p>","css":":root {\n  --nw-blue-600: #1f5ae0;\n  --nw-blue-50: #eaf0fd;\n  --nw-gray-200: #d9dee3;\n  --nw-gray-500: #7b8794;\n  --nw-color-action-bg: var(--nw-blue-600);\n  --nw-color-action-bg-subtle: var(--nw-blue-50);\n  --nw-color-text-disabled: var(--nw-gray-500);\n  --nw-color-border: var(--nw-gray-200);\n  --nw-focus-ring-color: var(--nw-blue-600);\n  --nw-focus-ring-width: 2px;\n  --nw-focus-ring-offset: 2px;\n}\n.nw-button {\n  --nw-button-bg: #ffffff;\n  --nw-button-text: #1f2933;\n  --nw-button-border: var(--nw-color-border);\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 1px solid var(--nw-button-border);\n  padding: 8px 16px;\n  border-radius: 6px;\n  font: inherit;\n  cursor: pointer;\n}\n.nw-button:focus {\n  outline: none;\n}\n","js":"let saves = 0;\nconst log = document.querySelector('#log');\nconst save = document.querySelector('#save');\nconst late = document.querySelector('#late');\n\nsave.addEventListener('click', () => {\n  saves += 1;\n  log.textContent = `Saved ${saves} times`;\n});\n\nlate.addEventListener('click', () => {\n  // Toggle the filter here\n});\n"},"solution":{"html":"<div class=\"toolbar\">\n  <button class=\"nw-button\" type=\"button\" id=\"assign\">Assign driver</button>\n  <button class=\"nw-button\" type=\"button\" id=\"late\" aria-pressed=\"false\">Show only late</button>\n  <button class=\"nw-button\" type=\"button\" id=\"save\" aria-disabled=\"true\" aria-describedby=\"save-why\">Save route</button>\n  <p id=\"save-why\">Select a driver before saving.</p>\n</div>\n<p id=\"log\">Saved 0 times</p>","css":":root {\n  --nw-blue-600: #1f5ae0;\n  --nw-blue-50: #eaf0fd;\n  --nw-gray-200: #d9dee3;\n  --nw-gray-500: #7b8794;\n  --nw-color-action-bg: var(--nw-blue-600);\n  --nw-color-action-bg-subtle: var(--nw-blue-50);\n  --nw-color-text-disabled: var(--nw-gray-500);\n  --nw-color-border: var(--nw-gray-200);\n  --nw-focus-ring-color: var(--nw-blue-600);\n  --nw-focus-ring-width: 2px;\n  --nw-focus-ring-offset: 2px;\n}\n.nw-button {\n  --nw-button-bg: #ffffff;\n  --nw-button-text: #1f2933;\n  --nw-button-border: var(--nw-color-border);\n  background: var(--nw-button-bg);\n  color: var(--nw-button-text);\n  border: 1px solid var(--nw-button-border);\n  padding: 8px 16px;\n  border-radius: 6px;\n  font: inherit;\n  cursor: pointer;\n}\n.nw-button:focus-visible {\n  outline: var(--nw-focus-ring-width) solid var(--nw-focus-ring-color);\n  outline-offset: var(--nw-focus-ring-offset);\n}\n.nw-button[aria-pressed=\"true\"] {\n  --nw-button-bg: var(--nw-color-action-bg-subtle);\n  --nw-button-border: var(--nw-color-action-bg);\n}\n.nw-button[aria-disabled=\"true\"] {\n  --nw-button-text: var(--nw-color-text-disabled);\n  cursor: not-allowed;\n}\n","js":"let saves = 0;\nconst log = document.querySelector('#log');\nconst save = document.querySelector('#save');\nconst late = document.querySelector('#late');\n\nsave.addEventListener('click', () => {\n  if (save.getAttribute('aria-disabled') === 'true') return;\n  saves += 1;\n  log.textContent = `Saved ${saves} times`;\n});\n\nlate.addEventListener('click', () => {\n  const pressed = late.getAttribute('aria-pressed') === 'true';\n  late.setAttribute('aria-pressed', String(!pressed));\n});\n"},"checks":[{"id":"focus-visible-rule","test":"const rules = [...document.styleSheets].flatMap((s) => [...s.cssRules]).filter((r) => r.selectorText); const fv = rules.filter((r) => r.selectorText.includes(':focus-visible')); const plainFocus = rules.filter((r) => /:focus(?![-\\w])/.test(r.selectorText) && /outline/.test(r.style.cssText) && /none|0px/.test(r.style.outlineStyle + r.style.outline + r.style.outlineWidth)); return fv.length > 0 && plainFocus.length === 0;"},{"id":"ring","test":"const b = $('#assign'); b.focus(); const ok = b.matches(':focus-visible') && style(b, 'outline-style') === 'solid' && style(b, 'outline-width') === '2px' && style(b, 'outline-color') === 'rgb(31, 90, 224)' && style(b, 'outline-offset') === '2px'; b.blur(); return ok;"},{"id":"toggle","test":"const b = $('#late'); click(b); await tick(); const on = b.getAttribute('aria-pressed') === 'true' && style(b, 'background-color') === 'rgb(234, 240, 253)'; click(b); await tick(); return on && b.getAttribute('aria-pressed') === 'false' && style(b, 'background-color') === 'rgb(255, 255, 255)';"},{"id":"blocked","test":"click($('#save')); await tick(); return text('#log') === 'Saved 0 times';"},{"id":"disabled-look","test":"return style('#save', 'color') === 'rgb(123, 135, 148)' && style('#save', 'cursor') === 'not-allowed';"}],"title":"Build focus, toggle and disabled states in","prompt":"<p>This toolbar hides keyboard focus and ignores its own states. Fix it in CSS and JS:</p>\n<ol>\n<li>Remove the <code dir=\"ltr\">:focus { outline: none }</code> rule and add a <code dir=\"ltr\">:focus-visible</code> ring: a solid outline using <code dir=\"ltr\">--nw-focus-ring-width</code>, <code dir=\"ltr\">--nw-focus-ring-color</code> and an <code dir=\"ltr\">outline-offset</code> of <code dir=\"ltr\">--nw-focus-ring-offset</code>.</li>\n<li>Make <strong>Show only late</strong> a real toggle: each click flips <code dir=\"ltr\">aria-pressed</code> between <code dir=\"ltr\">&quot;true&quot;</code> and <code dir=\"ltr\">&quot;false&quot;</code>. When pressed, its background is <code dir=\"ltr\">--nw-color-action-bg-subtle</code> and its border <code dir=\"ltr\">--nw-color-action-bg</code>.</li>\n<li><strong>Save route</strong> has <code dir=\"ltr\">aria-disabled=&quot;true&quot;</code>: clicking it must not save, its text uses <code dir=\"ltr\">--nw-color-text-disabled</code>, and its cursor is <code dir=\"ltr\">not-allowed</code>.</li>\n</ol>\n","hints":["Style states by their attributes: <code dir=\"ltr\">.nw-button[aria-pressed=&quot;true&quot;]</code> and <code dir=\"ltr\">.nw-button[aria-disabled=&quot;true&quot;]</code>. Override the component tokens inside them.","In the toggle’s click handler, read <code dir=\"ltr\">aria-pressed</code>, then set it to the opposite with <code dir=\"ltr\">setAttribute('aria-pressed', String(!pressed))</code>.","At the top of the save handler, <code dir=\"ltr\">return</code> early when <code dir=\"ltr\">save.getAttribute('aria-disabled') === 'true'</code>."],"explanation":"<p>The ring now appears for keyboard focus via <code dir=\"ltr\">:focus-visible</code> and comes from tokens, so every component and theme can share it. The toggle’s look is driven by <code dir=\"ltr\">aria-pressed</code>, so the visual state and the announced state can never disagree. <code dir=\"ltr\">aria-disabled</code> keeps Save focusable so its <code dir=\"ltr\">aria-describedby</code> explanation is reachable, which is why the click handler has to guard the action itself. In the real system, the Button component does all of this internally, and teams get it by default.</p>\n","checkLabels":{"focus-visible-rule":"Focus styling uses <code dir=\"ltr\">:focus-visible</code>, and no <code dir=\"ltr\">:focus</code> rule removes the outline","ring":"Keyboard focus shows a 2px solid ring in the focus color with a 2px offset","toggle":"Clicking <strong>Show only late</strong> toggles <code dir=\"ltr\">aria-pressed</code> and its pressed styling","blocked":"Clicking the <code dir=\"ltr\">aria-disabled</code> Save button doesn’t save","disabled-look":"Save uses the disabled text color and a <code dir=\"ltr\">not-allowed</code> cursor"}},"words":659,"readMinutes":3}