<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[A11Y in Practice]]></title><description><![CDATA[A11Y in Practice]]></description><link>https://a11yinpractice.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 29 Oct 2026 04:10:33 GMT</lastBuildDate><atom:link href="https://a11yinpractice.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[aria-selected on tabs: the selected tab that lies (WCAG 4.1.2)]]></title><description><![CDATA[Notifications is open, but VoiceOver says Account is selected. Missing and hard-coded aria-selected, the accessibility tree proof, and the fix.]]></description><link>https://a11yinpractice.hashnode.dev/aria-selected-tab-that-lies</link><guid isPermaLink="true">https://a11yinpractice.hashnode.dev/aria-selected-tab-that-lies</guid><category><![CDATA[Accessibility]]></category><category><![CDATA[#WCAG]]></category><category><![CDATA[aria]]></category><category><![CDATA[webdev]]></category><category><![CDATA[a11y]]></category><dc:creator><![CDATA[A11Y in Practice]]></dc:creator><pubDate>Wed, 28 Oct 2026 09:11:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ae1b625bace774ba735310b/f98167c2-8fbf-4643-8022-ec0b21f6714b.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Notifications tab is open. Its panel is on screen: "Choose which email and push alerts you receive." Then VoiceOver lands on the other tab and says Account, selected, tab, 1 of 2, Alert preferences, tab group. On Notifications, the tab you are looking at, it says Notifications, tab, 2 of 2. No selected. The tab you see is not the tab the code says.</p>
<p><a class="embed-card" href="https://youtu.be/939KZzpMFuk">https://youtu.be/939KZzpMFuk</a></p>

<p>WCAG 4.1.2 Name, Role, Value has three parts. In E07 (<a href="https://youtu.be/PJh5PMdxHwA">A Custom Div Is Not a Checkbox</a>), a custom control never told VoiceOver it was a checkbox - that was the role. This breakdown is the value. The name and the role are already right here (Account, tab, 1 of 2); the state is wrong.</p>
<p>This breakdown uses the Selected State lab (E10): Case 1 (no <code>aria-selected</code> at all) and Case 2 (<code>aria-selected</code> hard-coded). The evidence comes from real accessibility-tree and VoiceOver captures.</p>
<h2>The problem</h2>
<h3>Case 1 - <code>role="tab"</code> with no <code>aria-selected</code></h3>
<p>The tab list is real: <code>role="tablist"</code> with an <code>aria-label</code>, and two buttons with <code>role="tab"</code>. The active tab only gets a different background class. Nothing in the markup says which tab is selected:</p>
<pre><code class="language-html">&lt;div role="tablist" aria-label="Account settings" class="tab-bar"&gt;
  &lt;button type="button" role="tab" id="selected-state-broken-case1-account-tab"&gt;
    Account
  &lt;/button&gt;
  &lt;button
    type="button"
    role="tab"
    id="selected-state-broken-case1-notifications-tab"
  &gt;
    Notifications
  &lt;/button&gt;
&lt;/div&gt;
</code></pre>
<p>The selection lives in a CSS class. Assistive technology cannot read a class name.</p>
<h3>What VoiceOver announced (broken, Case 1)</h3>
<p>On first load, with Account visually active:</p>
<blockquote>
<p>Account, selected, tab, 1 of 2, Account settings, tab group</p>
</blockquote>
<blockquote>
<p>You are currently on a selected tab, 1 of 2.</p>
</blockquote>
<p>That sounds right, but there is no <code>aria-selected</code> in the markup. The capture notes say VoiceOver may announce selected here without a matching programmatic value in the tree. Do not build on it.</p>
<p>After activating Notifications, with the Notifications panel visible:</p>
<blockquote>
<p>Notifications, tab, 2 of 2</p>
</blockquote>
<blockquote>
<p>You are currently on a tab, 2 of 2. To select this option, press Control-Option-Space.</p>
</blockquote>
<p>It is asking you to select the tab you just selected.</p>
<h3>What the accessibility tree showed (broken, Case 1)</h3>
<p>Captured after activating Notifications:</p>
<ul>
<li><strong>tablist "Account settings"</strong>, multiselectable: false</li>
<li><strong>tab "Account"</strong> - focusable: true, <strong>selected: false</strong></li>
<li><strong>tab "Notifications"</strong> - focusable: true, <strong>selected: false</strong></li>
</ul>
<p>Two tabs. Neither one is selected. The screen shows an open tab; the code exposes none. Without <code>aria-selected</code>, the tree's selected state does not track the visual active tab.</p>
<h3>Case 2 - <code>aria-selected</code> hard-coded</h3>
<p>This is the partial fix. Someone read "add aria-selected" and added it - as literal values:</p>
<pre><code class="language-jsx">&lt;button
  type="button"
  role="tab"
  aria-selected={true}
  onClick={() =&gt; setCase2Active("account")}
  className={...}
&gt;
  Account
&lt;/button&gt;
&lt;button
  type="button"
  role="tab"
  aria-selected={false}
  onClick={() =&gt; setCase2Active("notifications")}
  className={...}
&gt;
  Notifications
&lt;/button&gt;
</code></pre>
<p>The click updates <code>case2Active</code>, which switches the styling and the panel. <code>aria-selected</code> never reads it. On first load that even looks correct, because Account is the starting tab. The bug only shows up when you switch.</p>
<h3>What VoiceOver announced (broken, Case 2)</h3>
<p>With Notifications focused and its panel visible:</p>
<blockquote>
<p>Notifications, tab, 2 of 2</p>
</blockquote>
<p>And on Account, which is no longer the open tab:</p>
<blockquote>
<p>Account, selected, tab, 1 of 2, Alert preferences, tab group</p>
</blockquote>
<h3>What the accessibility tree showed (broken, Case 2)</h3>
<ul>
<li><strong>tablist "Alert preferences"</strong>, multiselectable: false</li>
<li><strong>tab "Account"</strong> - focusable: true, <strong>selected: true</strong></li>
<li><strong>tab "Notifications"</strong> - focusable: true, <strong>focused: true</strong>, <strong>selected: false</strong></li>
</ul>
<p>The tree agrees with VoiceOver, and both disagree with the screen. Focus moved. Selection didn't.</p>
<h3>Why this fails exam-style</h3>
<ul>
<li><strong>4.1.2 Name, Role, Value</strong> (Level A) requires that the name and role of user interface components can be programmatically determined, that states the user can set can be programmatically set, and that changes to these items are available to assistive technologies. Which tab is selected is a state the user sets by clicking a tab. If the code never updates it, assistive technology is told the wrong thing.</li>
<li>The WAS rule for this (Custom Widgets, "Bound ARIA States"): "States such as <code>aria-expanded</code>, <code>aria-selected</code>, <code>aria-checked</code>, and <code>aria-pressed</code> MUST update when the UI changes - stale values mislead screen reader users."</li>
<li>Case 1 exposes <strong>no</strong> selected tab: both tabs are <code>selected: false</code> in the tree while one is visibly open.</li>
<li>Case 2 exposes the <strong>wrong</strong> selected tab. The attribute is present and its values are valid, so a quick check for the attribute finds it - but the value is stale.</li>
<li>Exam signal for missing state: a tab list where no tab has <code>selected: true</code> while one panel is visible. Exam signal for stale state: <code>selected: true</code> on a tab that is not the open one, often next to <code>focused: true</code> on the tab that is. In both cases, test <strong>after switching</strong>, and check both tabs - not only the first one.</li>
<li>Name and role problems (a control that does not expose what it is) are the E07 side of 4.1.2. <code>role="tabpanel"</code> / <code>aria-controls</code> and arrow-key navigation are separate topics (Panel Association and Full Tabs Pattern labs).</li>
</ul>
<h2>The fix</h2>
<p>Bind <code>aria-selected</code> to the same state that drives the active styling and the panel: <code>true</code> on the active tab, <code>false</code> on every other tab. The same fix repairs both cases. In React:</p>
<pre><code class="language-jsx">&lt;button
  type="button"
  role="tab"
  id="selected-state-fixed-case1-account-tab"
  aria-selected={case1Active === "account"}
  onClick={() =&gt; setCase1Active("account")}
  className={...}
&gt;
  Account
&lt;/button&gt;
&lt;button
  type="button"
  role="tab"
  id="selected-state-fixed-case1-notifications-tab"
  aria-selected={case1Active === "notifications"}
  onClick={() =&gt; setCase1Active("notifications")}
  className={...}
&gt;
  Notifications
&lt;/button&gt;
</code></pre>
<p>Without a framework, update the attribute in the same place you switch the tab:</p>
<pre><code class="language-js">const accountTab = document.querySelector(
  "#selected-state-fixed-case1-account-tab",
);
const notificationsTab = document.querySelector(
  "#selected-state-fixed-case1-notifications-tab",
);

function setActiveTab(id) {
  accountTab.setAttribute("aria-selected", String(id === "account"));
  notificationsTab.setAttribute("aria-selected", String(id === "notifications"));
}

accountTab.addEventListener("click", () =&gt; setActiveTab("account"));
notificationsTab.addEventListener("click", () =&gt; setActiveTab("notifications"));
</code></pre>
<p>Not a CSS class. Not a hard-coded <code>true</code>.</p>
<h3>What changed in the tree (fixed)</h3>
<p>With the Notifications panel visible in both cases:</p>
<ul>
<li>Case 1: <strong>tab "Account"</strong> - focusable: true, <strong>selected: false</strong>; <strong>tab "Notifications"</strong> - focusable: true, <strong>selected: true</strong>. The Notifications node lists <code>aria-selected</code>: true, the Account node <code>aria-selected</code>: false.</li>
<li>Case 2: <strong>tab "Account"</strong> - focusable: true, <strong>selected: false</strong>; <strong>tab "Notifications"</strong> - focusable: true, <strong>focused: true</strong>, <strong>selected: true</strong>.</li>
<li>Contrast broken Case 2, where Account stayed <strong>selected: true</strong> after Notifications became active.</li>
</ul>
<p>Exactly one tab is selected, and it is the one on screen. The tree finally matches the page.</p>
<h3>What VoiceOver announced (fixed)</h3>
<p>Case 2, with Account active:</p>
<blockquote>
<p>Account, selected, tab, 1 of 2, Alert preferences, tab group</p>
</blockquote>
<p>The hint on the active Account tab (Case 1):</p>
<blockquote>
<p>You are currently on a selected tab, 1 of 2.</p>
</blockquote>
<p>The capture also records a state-only reading for Case 2 (the lab's evidence caption places it on the active Notifications tab):</p>
<blockquote>
<p>selected</p>
</blockquote>
<p>To be honest about the evidence: the lab capture does not include a full VoiceOver announcement for the active Notifications tab after the fix, so this post does not quote one. The proof for the fix is the tree above - <code>selected: true</code> on exactly the visible tab. While Account is active, the inactive Notifications tab reads:</p>
<blockquote>
<p>Notifications, tab, 2 of 2</p>
</blockquote>
<blockquote>
<p>You are currently on a tab, 2 of 2. To select this option, press Control-Option-Space.</p>
</blockquote>
<h3>Behaviors to verify</h3>
<ol>
<li>Exactly one tab in each tab list has <code>aria-selected="true"</code>; every other tab has <code>aria-selected="false"</code>.</li>
<li><code>aria-selected</code> reads the same state that drives the active styling and the visible panel - no literal <code>true</code> or <code>false</code>.</li>
<li>Switch tabs, then check the accessibility tree: <code>selected: true</code> must follow the visible tab. Then move VoiceOver to both tabs, not just the first one, and confirm only the open tab is announced as selected. Do not trust a first-load listen.</li>
</ol>
<h2>Practice it</h2>
<ul>
<li>Video: <a href="https://youtu.be/939KZzpMFuk">https://youtu.be/939KZzpMFuk</a></li>
<li>Short: <a href="https://youtube.com/shorts/l2yr49-N8b8">https://youtube.com/shorts/l2yr49-N8b8</a></li>
<li>Lab: <a href="https://www.wasexamprep.com/go/e10-lab-hashnode">Selected State lab</a></li>
<li>Original write-up on WAS Lab: <a href="https://www.wasexamprep.com/go/e10-blog-hashnode">https://www.wasexamprep.com/go/e10-blog-hashnode</a></li>
</ul>
<p>Takeaway: present is not correct. A tab's <code>aria-selected</code> has to follow the tab that is actually open. Switch tabs, and check the tree, not just your ears.</p>
<p>If this was useful, subscribe to <strong>A11Y in Practice</strong> on YouTube (<a href="https://www.youtube.com/@a11yinpractice">@a11yinpractice</a>) for more hands-on accessibility episodes, and visit <a href="https://www.wasexamprep.com">wasexamprep.com</a> for WAS study notes and labs.</p>
<p><em>Independent study notes - not affiliated with IAAP or W3C.</em></p>
]]></content:encoded></item></channel></rss>