<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ARIA on Tarragon</title><link>https://tarrragon.github.io/blog/tags/aria/</link><description>Recent content in ARIA on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 25 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/aria/index.xml" rel="self" type="application/rss+xml"/><item><title>Screen reader 與動態內容變動的 live region 設計</title><link>https://tarrragon.github.io/blog/report/aria-live-for-dynamic-content/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/aria-live-for-dynamic-content/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>動態內容變動對螢幕報讀軟體使用者預設不可見 — 要主動透過 aria-live region 把變動「廣播」給輔助技術。&lt;/strong> 沒 live region 的 UI 在視覺使用者眼裡很流暢、在 screen reader 使用者眼裡是「靜悄悄變了什麼但我不知道」。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼動態內容需要主動廣播">為什麼動態內容需要主動廣播&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>Screen reader 的工作模式：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>動作&lt;/th>
 &lt;th>screen reader 行為&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>頁面載入&lt;/td>
 &lt;td>朗讀整個 main 內容（或使用者導航位置）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者按 tab&lt;/td>
 &lt;td>朗讀新 focus 元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者按方向鍵&lt;/td>
 &lt;td>朗讀附近元素&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>DOM 變動但 focus 沒動&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>預設不朗讀&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>第四種是動態 UI 的常見情境 — 使用者在 search input 上、結果列表變動 — screen reader 預設不知道。&lt;/p>
&lt;p>&lt;code>aria-live&lt;/code> 屬性告訴 screen reader「這個區域內容變動時、主動朗讀變動」。沒 aria-live、變動就沉默。&lt;/p>
&lt;h3 id="三類動態變動">三類動態變動&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>變動類型&lt;/th>
 &lt;th>是否需要廣播&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>使用者主動觸發、focus 跟著走&lt;/td>
 &lt;td>否（focus 變動會朗讀新位置）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>使用者主動觸發、focus 沒動&lt;/td>
 &lt;td>是 — 用 &lt;code>aria-live=&amp;quot;polite&amp;quot;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>重要警示（錯誤訊息、警告）&lt;/td>
 &lt;td>是 — 用 &lt;code>aria-live=&amp;quot;assertive&amp;quot;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>polite&lt;/code> 等使用者當前朗讀完才宣告、&lt;code>assertive&lt;/code> 立刻打斷 — 多數場景用 polite。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1scope-filter-切換後沒提示">風險 1：Scope filter 切換後沒提示&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：scope filter 的 &lt;code>apply()&lt;/code> — 改變 result 顯示後沒有 aria 提示。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：使用者切換 scope（標題 / 內文 / 全部）、UI 上結果數量變了 — screen reader 完全不知道。使用者可能繼續以為「189 筆結果」、實際只剩 4 筆。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：screen reader 使用者切 scope 後、tab 到結果區、發現跟剛才不同、困惑。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：加 aria-live region 在 scope UI 旁邊、apply 後寫入訊息。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span> &lt;span class="na">role&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;radiogroup&amp;#34;&lt;/span> &lt;span class="na">aria-label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;搜尋範圍&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- radios... --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope-status&amp;#34;&lt;/span> &lt;span class="na">aria-live&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;polite&amp;#34;&lt;/span> &lt;span class="na">aria-atomic&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;true&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kd">function&lt;/span> &lt;span class="nx">apply&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="c1">// ... filter 邏輯
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span> &lt;span class="kd">var&lt;/span> &lt;span class="nx">visible&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">filter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">el&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="o">!&lt;/span>&lt;span class="nx">el&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">classList&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">contains&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;is-scope-filtered&amp;#39;&lt;/span>&lt;span class="p">)).&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">querySelector&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;.search-scope-status&amp;#39;&lt;/span>&lt;span class="p">).&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s1">&amp;#39;篩選後 &amp;#39;&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">visible&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; 筆結果&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>aria-atomic=&amp;quot;true&amp;quot;&lt;/code> 確保整個訊息每次都完整朗讀（而非只朗讀差異）。&lt;/p>
&lt;h3 id="風險-2搜尋結果載入完成沒提示">風險 2：搜尋結果載入完成沒提示&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：使用者打字、pagefind 載入結果 — UI 上 result 出現、但 screen reader 不知道載入完成。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：使用者打字結束、預期「結果出來了」— 但需要主動 tab 過去確認、不像視覺使用者一眼看到。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：使用者打字後不知道結果是否準備好、不知道是否該 tab 過去。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：pagefind 自身可能已實作 aria-live；若未、加一個 region 在結果區上方。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-results-status&amp;#34;&lt;/span> &lt;span class="na">aria-live&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;polite&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-js" data-lang="js">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">new&lt;/span> &lt;span class="nx">MutationObserver&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">function&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kd">var&lt;/span> &lt;span class="nx">count&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">items&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">length&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nx">status&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">textContent&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">count&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="s1">&amp;#39; 筆結果符合搜尋&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">}).&lt;/span>&lt;span class="nx">observe&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">resultsRoot&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">childList&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="kc">true&lt;/span> &lt;span class="p">});&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="風險-3filter-變動後沒提示">風險 3：Filter 變動後沒提示&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：使用者勾選 / 取消 filter checkbox、pagefind 自動更新結果。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：勾選某個 tag、結果列表變動 — screen reader 看不到變動、若 focus 還在 checkbox 也沒朗讀。&lt;/p>
&lt;p>&lt;strong>症狀&lt;/strong>：螢幕報讀軟體使用者勾 filter、不知道有沒有效果。&lt;/p>
&lt;p>&lt;strong>第一個該查的&lt;/strong>：同上、aria-live region 反映「N 筆結果符合篩選」。&lt;/p>
&lt;h3 id="風險-4無結果訊息">風險 4：「無結果」訊息&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：搜尋字找不到任何結果。&lt;/p>
&lt;p>&lt;strong>判讀&lt;/strong>：頁面顯示「找不到 X 相關內容」、screen reader 若 focus 還在 input 不會朗讀。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>動態內容變動對螢幕報讀軟體使用者預設不可見 — 要主動透過 aria-live region 把變動「廣播」給輔助技術。</strong> 沒 live region 的 UI 在視覺使用者眼裡很流暢、在 screen reader 使用者眼裡是「靜悄悄變了什麼但我不知道」。</p>
<hr>
<h2 id="為什麼動態內容需要主動廣播">為什麼動態內容需要主動廣播</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>Screen reader 的工作模式：</p>
<table>
  <thead>
      <tr>
          <th>動作</th>
          <th>screen reader 行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>頁面載入</td>
          <td>朗讀整個 main 內容（或使用者導航位置）</td>
      </tr>
      <tr>
          <td>使用者按 tab</td>
          <td>朗讀新 focus 元素</td>
      </tr>
      <tr>
          <td>使用者按方向鍵</td>
          <td>朗讀附近元素</td>
      </tr>
      <tr>
          <td><strong>DOM 變動但 focus 沒動</strong></td>
          <td><strong>預設不朗讀</strong></td>
      </tr>
  </tbody>
</table>
<p>第四種是動態 UI 的常見情境 — 使用者在 search input 上、結果列表變動 — screen reader 預設不知道。</p>
<p><code>aria-live</code> 屬性告訴 screen reader「這個區域內容變動時、主動朗讀變動」。沒 aria-live、變動就沉默。</p>
<h3 id="三類動態變動">三類動態變動</h3>
<table>
  <thead>
      <tr>
          <th>變動類型</th>
          <th>是否需要廣播</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>使用者主動觸發、focus 跟著走</td>
          <td>否（focus 變動會朗讀新位置）</td>
      </tr>
      <tr>
          <td>使用者主動觸發、focus 沒動</td>
          <td>是 — 用 <code>aria-live=&quot;polite&quot;</code></td>
      </tr>
      <tr>
          <td>重要警示（錯誤訊息、警告）</td>
          <td>是 — 用 <code>aria-live=&quot;assertive&quot;</code></td>
      </tr>
  </tbody>
</table>
<p><code>polite</code> 等使用者當前朗讀完才宣告、<code>assertive</code> 立刻打斷 — 多數場景用 polite。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1scope-filter-切換後沒提示">風險 1：Scope filter 切換後沒提示</h3>
<p><strong>位置</strong>：scope filter 的 <code>apply()</code> — 改變 result 顯示後沒有 aria 提示。</p>
<p><strong>判讀</strong>：使用者切換 scope（標題 / 內文 / 全部）、UI 上結果數量變了 — screen reader 完全不知道。使用者可能繼續以為「189 筆結果」、實際只剩 4 筆。</p>
<p><strong>症狀</strong>：screen reader 使用者切 scope 後、tab 到結果區、發現跟剛才不同、困惑。</p>
<p><strong>第一個該查的</strong>：加 aria-live region 在 scope UI 旁邊、apply 後寫入訊息。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">role</span><span class="o">=</span><span class="s">&#34;radiogroup&#34;</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;搜尋範圍&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c">&lt;!-- radios... --&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope-status&#34;</span> <span class="na">aria-live</span><span class="o">=</span><span class="s">&#34;polite&#34;</span> <span class="na">aria-atomic</span><span class="o">=</span><span class="s">&#34;true&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">apply</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// ... filter 邏輯
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">visible</span> <span class="o">=</span> <span class="nx">items</span><span class="p">.</span><span class="nx">filter</span><span class="p">(</span><span class="nx">el</span> <span class="p">=&gt;</span> <span class="o">!</span><span class="nx">el</span><span class="p">.</span><span class="nx">classList</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="s1">&#39;is-scope-filtered&#39;</span><span class="p">)).</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;.search-scope-status&#39;</span><span class="p">).</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;篩選後 &#39;</span> <span class="o">+</span> <span class="nx">visible</span> <span class="o">+</span> <span class="s1">&#39; 筆結果&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>aria-atomic=&quot;true&quot;</code> 確保整個訊息每次都完整朗讀（而非只朗讀差異）。</p>
<h3 id="風險-2搜尋結果載入完成沒提示">風險 2：搜尋結果載入完成沒提示</h3>
<p><strong>位置</strong>：使用者打字、pagefind 載入結果 — UI 上 result 出現、但 screen reader 不知道載入完成。</p>
<p><strong>判讀</strong>：使用者打字結束、預期「結果出來了」— 但需要主動 tab 過去確認、不像視覺使用者一眼看到。</p>
<p><strong>症狀</strong>：使用者打字後不知道結果是否準備好、不知道是否該 tab 過去。</p>
<p><strong>第一個該查的</strong>：pagefind 自身可能已實作 aria-live；若未、加一個 region 在結果區上方。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-results-status&#34;</span> <span class="na">aria-live</span><span class="o">=</span><span class="s">&#34;polite&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">new</span> <span class="nx">MutationObserver</span><span class="p">(</span><span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kd">var</span> <span class="nx">count</span> <span class="o">=</span> <span class="nx">items</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nx">status</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">count</span> <span class="o">+</span> <span class="s1">&#39; 筆結果符合搜尋&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}).</span><span class="nx">observe</span><span class="p">(</span><span class="nx">resultsRoot</span><span class="p">,</span> <span class="p">{</span> <span class="nx">childList</span><span class="o">:</span> <span class="kc">true</span> <span class="p">});</span></span></span></code></pre></div><h3 id="風險-3filter-變動後沒提示">風險 3：Filter 變動後沒提示</h3>
<p><strong>位置</strong>：使用者勾選 / 取消 filter checkbox、pagefind 自動更新結果。</p>
<p><strong>判讀</strong>：勾選某個 tag、結果列表變動 — screen reader 看不到變動、若 focus 還在 checkbox 也沒朗讀。</p>
<p><strong>症狀</strong>：螢幕報讀軟體使用者勾 filter、不知道有沒有效果。</p>
<p><strong>第一個該查的</strong>：同上、aria-live region 反映「N 筆結果符合篩選」。</p>
<h3 id="風險-4無結果訊息">風險 4：「無結果」訊息</h3>
<p><strong>位置</strong>：搜尋字找不到任何結果。</p>
<p><strong>判讀</strong>：頁面顯示「找不到 X 相關內容」、screen reader 若 focus 還在 input 不會朗讀。</p>
<p><strong>症狀</strong>：screen reader 使用者打字後沒任何回應、不知道是「無結果」還是「還在搜尋」。</p>
<p><strong>第一個該查的</strong>：把「無結果」訊息放 aria-live region 內、變動時自動朗讀。</p>
<hr>
<h2 id="live-region-的設計選擇">Live region 的設計選擇</h2>
<h3 id="polite-vs-assertive"><code>polite</code> vs <code>assertive</code></h3>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>行為</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>aria-live=&quot;polite&quot;</code></td>
          <td>等使用者當前朗讀完才宣告</td>
          <td>多數動態變動</td>
      </tr>
      <tr>
          <td><code>aria-live=&quot;assertive&quot;</code></td>
          <td>立刻打斷使用者朗讀</td>
          <td>錯誤、警告、緊急訊息</td>
      </tr>
  </tbody>
</table>
<p>優先 polite — assertive 容易打斷使用者、感覺很突兀。</p>
<h3 id="aria-atomic"><code>aria-atomic</code></h3>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>aria-atomic=&quot;false&quot;</code>（預設）</td>
          <td>只朗讀變動的部分</td>
      </tr>
      <tr>
          <td><code>aria-atomic=&quot;true&quot;</code></td>
          <td>整個 region 內容完整朗讀</td>
      </tr>
  </tbody>
</table>
<p>對「N 筆結果」這類固定格式訊息、用 <code>aria-atomic=&quot;true&quot;</code> 確保使用者聽到完整脈絡（不只朗讀數字變動）。</p>
<h3 id="aria-relevant"><code>aria-relevant</code></h3>
<p>預設只朗讀「新增 / 文字變動」、不朗讀「移除」。多數情境用預設即可。</p>
<hr>
<h2 id="內在屬性比較四種動態內容廣播策略">內在屬性比較：四種動態內容廣播策略</h2>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>涵蓋情境</th>
          <th>維護成本</th>
          <th>適用</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>不處理（沉默）</td>
          <td>不適用</td>
          <td>0</td>
          <td>不適用</td>
      </tr>
      <tr>
          <td><code>aria-live=&quot;polite&quot;</code></td>
          <td>大多數動態變動</td>
          <td>低 — 加 div 與 textContent 寫入</td>
          <td>預設</td>
      </tr>
      <tr>
          <td><code>aria-live=&quot;assertive&quot;</code></td>
          <td>緊急訊息</td>
          <td>低</td>
          <td>錯誤 / 警告</td>
      </tr>
      <tr>
          <td><code>role=&quot;status&quot;</code> / <code>role=&quot;alert&quot;</code></td>
          <td>semantic 角色明確</td>
          <td>低</td>
          <td>純 status / alert 元素</td>
      </tr>
  </tbody>
</table>
<p>優先選 <code>aria-live=&quot;polite&quot;</code> + <code>aria-atomic=&quot;true&quot;</code>、廣覆蓋且不打擾。</p>
<hr>
<h2 id="live-region-的常見錯誤">Live region 的常見錯誤</h2>
<h3 id="1-動態建立-region">1. 動態建立 region</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-js" data-lang="js"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">var</span> <span class="nx">status</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">status</span><span class="p">.</span><span class="nx">setAttribute</span><span class="p">(</span><span class="s1">&#39;aria-live&#39;</span><span class="p">,</span> <span class="s1">&#39;polite&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="nx">status</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s1">&#39;...&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">status</span><span class="p">);</span></span></span></code></pre></div><p>不會生效 — screen reader 在 region 出現「之後」變動才朗讀、region 從無到有的瞬間不算。</p>
<p>正確：region 在 HTML 預先存在、JS 只更新內容。</p>
<h3 id="2-全頁加一個共用-region">2. 全頁加一個共用 region</h3>
<p>可能導致訊息混淆 — 不同 source 的訊息共用同一個 region、難以追蹤。每個語意區域有自己的 region 較清楚。</p>
<h3 id="3-太頻繁的訊息">3. 太頻繁的訊息</h3>
<p>每次變動都廣播 — 使用者被 spam。Debounce + 重複內容跳過。</p>
<hr>
<h2 id="設計取捨動態內容廣播策略">設計取捨：動態內容廣播策略</h2>
<p>四種做法、各自機會成本不同。這個專案選 A（aria-live polite + aria-atomic）當預設、其他做法在特定情境合理。</p>
<h3 id="aaria-livepolite--aria-atomictrue這個專案的預設">A：<code>aria-live=&quot;polite&quot;</code> + <code>aria-atomic=&quot;true&quot;</code>（這個專案的預設）</h3>
<ul>
<li><strong>機制</strong>：region 預先在 HTML、JS 寫入 textContent 觸發 polite 朗讀（等使用者當前朗讀完）</li>
<li><strong>選 A 的理由</strong>：覆蓋多數動態變動、不打擾使用者當前操作</li>
<li><strong>適合</strong>：搜尋結果數量變動、filter 切換、scope 改變等大多數 UI 變動</li>
<li><strong>代價</strong>：訊息要等使用者當前朗讀完才聽到（最多幾秒延遲）</li>
</ul>
<h3 id="baria-liveassertive">B：<code>aria-live=&quot;assertive&quot;</code></h3>
<ul>
<li><strong>機制</strong>：立刻打斷使用者當前朗讀、強制聽新訊息</li>
<li><strong>跟 A 的取捨</strong>：B 即時、A 禮貌；但 B 打斷感強、頻繁使用會讓使用者疲勞</li>
<li><strong>B 比 A 好的情境</strong>：真正緊急的訊息（錯誤 / 警告 / 安全提示）— 必須立刻知道</li>
</ul>
<h3 id="crolestatus--rolealert">C：<code>role=&quot;status&quot;</code> / <code>role=&quot;alert&quot;</code></h3>
<ul>
<li><strong>機制</strong>：用 semantic role 取代 aria-live、語意更明確</li>
<li><strong>跟 A 的取捨</strong>：C 跟 A 行為類似（status = polite、alert = assertive）、但 role 表達意圖更清楚</li>
<li><strong>C 比 A 好的情境</strong>：region 本身就是 status 或 alert 元素（語意對齊）</li>
</ul>
<h3 id="d不處理沉默">D：不處理（沉默）</h3>
<ul>
<li><strong>機制</strong>：DOM 變動不通知 screen reader</li>
<li><strong>成本特別高的原因</strong>：screen reader 使用者完全不知道有變動、UI 變得不可用</li>
<li><strong>D 才合理的情境</strong>：純視覺裝飾變動（背景動畫 / decorative）— 對 screen reader 使用者無意義</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>該檢查的位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Screen reader 使用者反映「不知道有沒有發生事」</td>
          <td>找出該變動位置、加 aria-live region</td>
      </tr>
      <tr>
          <td>動態 UI 沒任何 aria-live</td>
          <td>列出所有 focus 不跟著走的變動、各自評估是否需要</td>
      </tr>
      <tr>
          <td>Live region 朗讀但聽起來只有片段</td>
          <td>加 <code>aria-atomic=&quot;true&quot;</code></td>
      </tr>
      <tr>
          <td>訊息太頻繁打擾</td>
          <td>Debounce、跳過重複</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：UI 上「使用者沒主動觸發但有變動」的位置、screen reader 預設沉默 — 用 aria-live region 把沉默變成可聽見。</p>
<p>跟 <a href="../loading-empty-end-state-distinction/">#57 Loading / Empty / End 三狀態的區分</a> 同源：兩者都是「狀態變動需要告知使用者」、aria-live 告訴的是 screen reader、#57 講的是視覺區分。<strong>完整的狀態變動 UX = 視覺區分 + aria-live 廣播</strong>。</p>
]]></content:encoded></item><item><title>Native HTML element 優先於 ARIA role 的取捨</title><link>https://tarrragon.github.io/blog/report/native-html-over-aria-role/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/report/native-html-over-aria-role/</guid><description>&lt;h2 id="核心原則">核心原則&lt;/h2>
&lt;p>&lt;strong>有 native HTML element 提供的語意與行為時、永遠優先用 native — ARIA role 是「沒有 native 對應時的 fallback」、不是設計起點。&lt;/strong> Native element 自帶 keyboard、focus、screen reader 行為；ARIA 是給作者宣告 semantic 的工具、需要作者自己補完所有行為。&lt;/p>
&lt;hr>
&lt;h2 id="為什麼-native-永遠優先">為什麼 native 永遠優先&lt;/h2>
&lt;h3 id="商業邏輯">商業邏輯&lt;/h3>
&lt;p>ARIA 規範自己有一條 first rule：&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>First Rule of ARIA&lt;/strong>: If you can use a native HTML element with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, do so.&lt;/p>&lt;/blockquote>
&lt;p>理由是 native element 提供「semantic + behavior」雙包裝：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>包裝層&lt;/th>
 &lt;th>Native element&lt;/th>
 &lt;th>ARIA role&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Semantic（screen reader 知道是什麼）&lt;/td>
 &lt;td>是&lt;/td>
 &lt;td>是&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>鍵盤行為&lt;/td>
 &lt;td>是（瀏覽器內建）&lt;/td>
 &lt;td>否（要作者自己寫）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Focus 行為&lt;/td>
 &lt;td>是（tab order、:focus）&lt;/td>
 &lt;td>否（要作者管 tabindex）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Form 整合&lt;/td>
 &lt;td>是（form submission、validation）&lt;/td>
 &lt;td>否&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨瀏覽器一致&lt;/td>
 &lt;td>高（標準行為）&lt;/td>
 &lt;td>中（看 screen reader 解讀）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>ARIA role 只貼 semantic 標籤、不送行為 — 用 ARIA 等於承擔「補完所有行為」的責任。&lt;/p>
&lt;h3 id="何時-aria-是必要">何時 ARIA 是必要&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>情境&lt;/th>
 &lt;th>ARIA 必要&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Native element 有對應功能&lt;/td>
 &lt;td>否 — 用 native&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>需要 semantic 但沒 native 對應&lt;/td>
 &lt;td>是 — 用 ARIA role&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>加強 native element 的描述（aria-label）&lt;/td>
 &lt;td>是 — ARIA 補強、不取代&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>動態狀態（aria-expanded、aria-checked）&lt;/td>
 &lt;td>是 — native 表達不了&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>ARIA 的設計用途是「補強 native」、不是「取代 native」。&lt;/p>
&lt;hr>
&lt;h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點&lt;/h2>
&lt;h3 id="風險-1scope-ui-用-div-roleradiogroup-而非-fieldset">風險 1：Scope UI 用 &lt;code>div role=&amp;quot;radiogroup&amp;quot;&lt;/code> 而非 &lt;code>fieldset&lt;/code>&lt;/h3>
&lt;p>&lt;strong>位置&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">class&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span> &lt;span class="na">role&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;radiogroup&amp;#34;&lt;/span> &lt;span class="na">aria-label&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;搜尋範圍&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">label&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;&lt;/span>&lt;span class="nt">input&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;radio&amp;#34;&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;search-scope&amp;#34;&lt;/span> &lt;span class="na">value&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;all&amp;#34;&lt;/span> &lt;span class="na">checked&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>全部&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">span&lt;/span>&lt;span class="p">&amp;gt;&amp;lt;/&lt;/span>&lt;span class="nt">label&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="c">&amp;lt;!-- ... --&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>判讀&lt;/strong>：&lt;code>&amp;lt;div role=&amp;quot;radiogroup&amp;quot;&amp;gt;&lt;/code> 給 screen reader 看到「這是 radio group」、但作者要自己保證：&lt;/p>
&lt;ul>
&lt;li>鍵盤方向鍵在選項間切換&lt;/li>
&lt;li>Tab 行為符合 radiogroup 慣例（tab 進到 group、方向鍵在內切換、tab 出 group）&lt;/li>
&lt;li>aria-required / aria-invalid 等狀態同步&lt;/li>
&lt;/ul>
&lt;p>&lt;code>&amp;lt;fieldset&amp;gt;&amp;lt;legend&amp;gt;&lt;/code> 是 native element：&lt;/p>
&lt;ul>
&lt;li>自帶 group semantic&lt;/li>
&lt;li>legend 自動關聯為 group label&lt;/li>
&lt;li>內部 &lt;code>&amp;lt;input type=&amp;quot;radio&amp;quot; name=&amp;quot;X&amp;quot;&amp;gt;&lt;/code> 已是完整 radiogroup（HTML 內建）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>症狀&lt;/strong>：screen reader 可能不認得自訂 radiogroup、無法用方向鍵切換。&lt;/p></description><content:encoded><![CDATA[<h2 id="核心原則">核心原則</h2>
<p><strong>有 native HTML element 提供的語意與行為時、永遠優先用 native — ARIA role 是「沒有 native 對應時的 fallback」、不是設計起點。</strong> Native element 自帶 keyboard、focus、screen reader 行為；ARIA 是給作者宣告 semantic 的工具、需要作者自己補完所有行為。</p>
<hr>
<h2 id="為什麼-native-永遠優先">為什麼 native 永遠優先</h2>
<h3 id="商業邏輯">商業邏輯</h3>
<p>ARIA 規範自己有一條 first rule：</p>
<blockquote>
<p><strong>First Rule of ARIA</strong>: If you can use a native HTML element with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, do so.</p></blockquote>
<p>理由是 native element 提供「semantic + behavior」雙包裝：</p>
<table>
  <thead>
      <tr>
          <th>包裝層</th>
          <th>Native element</th>
          <th>ARIA role</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Semantic（screen reader 知道是什麼）</td>
          <td>是</td>
          <td>是</td>
      </tr>
      <tr>
          <td>鍵盤行為</td>
          <td>是（瀏覽器內建）</td>
          <td>否（要作者自己寫）</td>
      </tr>
      <tr>
          <td>Focus 行為</td>
          <td>是（tab order、:focus）</td>
          <td>否（要作者管 tabindex）</td>
      </tr>
      <tr>
          <td>Form 整合</td>
          <td>是（form submission、validation）</td>
          <td>否</td>
      </tr>
      <tr>
          <td>跨瀏覽器一致</td>
          <td>高（標準行為）</td>
          <td>中（看 screen reader 解讀）</td>
      </tr>
  </tbody>
</table>
<p>ARIA role 只貼 semantic 標籤、不送行為 — 用 ARIA 等於承擔「補完所有行為」的責任。</p>
<h3 id="何時-aria-是必要">何時 ARIA 是必要</h3>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>ARIA 必要</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Native element 有對應功能</td>
          <td>否 — 用 native</td>
      </tr>
      <tr>
          <td>需要 semantic 但沒 native 對應</td>
          <td>是 — 用 ARIA role</td>
      </tr>
      <tr>
          <td>加強 native element 的描述（aria-label）</td>
          <td>是 — ARIA 補強、不取代</td>
      </tr>
      <tr>
          <td>動態狀態（aria-expanded、aria-checked）</td>
          <td>是 — native 表達不了</td>
      </tr>
  </tbody>
</table>
<p>ARIA 的設計用途是「補強 native」、不是「取代 native」。</p>
<hr>
<h2 id="搜尋頁的具體風險點">搜尋頁的具體風險點</h2>
<h3 id="風險-1scope-ui-用-div-roleradiogroup-而非-fieldset">風險 1：Scope UI 用 <code>div role=&quot;radiogroup&quot;</code> 而非 <code>fieldset</code></h3>
<p><strong>位置</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">role</span><span class="o">=</span><span class="s">&#34;radiogroup&#34;</span> <span class="na">aria-label</span><span class="o">=</span><span class="s">&#34;搜尋範圍&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;all&#34;</span> <span class="na">checked</span><span class="p">&gt;&lt;</span><span class="nt">span</span><span class="p">&gt;</span>全部<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="c">&lt;!-- ... --&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span></span></span></code></pre></div><p><strong>判讀</strong>：<code>&lt;div role=&quot;radiogroup&quot;&gt;</code> 給 screen reader 看到「這是 radio group」、但作者要自己保證：</p>
<ul>
<li>鍵盤方向鍵在選項間切換</li>
<li>Tab 行為符合 radiogroup 慣例（tab 進到 group、方向鍵在內切換、tab 出 group）</li>
<li>aria-required / aria-invalid 等狀態同步</li>
</ul>
<p><code>&lt;fieldset&gt;&lt;legend&gt;</code> 是 native element：</p>
<ul>
<li>自帶 group semantic</li>
<li>legend 自動關聯為 group label</li>
<li>內部 <code>&lt;input type=&quot;radio&quot; name=&quot;X&quot;&gt;</code> 已是完整 radiogroup（HTML 內建）</li>
</ul>
<p><strong>症狀</strong>：screen reader 可能不認得自訂 radiogroup、無法用方向鍵切換。</p>
<p><strong>第一個該查的</strong>：用 NVDA / VoiceOver 進入 radiogroup、按方向鍵看是否能切換。失敗則改用 fieldset。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">&lt;</span><span class="nt">fieldset</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="p">&lt;</span><span class="nt">legend</span><span class="p">&gt;</span>搜尋範圍<span class="p">&lt;/</span><span class="nt">legend</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;all&#34;</span> <span class="na">checked</span><span class="p">&gt;</span> 全部<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;title&#34;</span><span class="p">&gt;</span> 標題<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="p">&lt;</span><span class="nt">label</span><span class="p">&gt;&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;radio&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;search-scope&#34;</span> <span class="na">value</span><span class="o">=</span><span class="s">&#34;content&#34;</span><span class="p">&gt;</span> 內文<span class="p">&lt;/</span><span class="nt">label</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="p">&lt;/</span><span class="nt">fieldset</span><span class="p">&gt;</span></span></span></code></pre></div><p><code>name=&quot;search-scope&quot;</code> 同名讓三個 radio 自動成為 group、HTML 自帶方向鍵切換。</p>
<h3 id="風險-2用-div-onclick-取代-button">風險 2：用 <code>&lt;div onclick&gt;</code> 取代 <code>&lt;button&gt;</code></h3>
<p><strong>位置</strong>：自訂按鈕 UI（搜尋頁未必有、但常見 anti-pattern）。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>&lt;button&gt;</code> 自帶 enter / space 觸發、tab focus、disabled 狀態</li>
<li><code>&lt;div onclick&gt;</code> 只有 click 事件、鍵盤無法觸發、tab 不會 focus</li>
</ul>
<p><strong>症狀</strong>：鍵盤使用者無法操作該 UI。</p>
<p><strong>第一個該查的</strong>：找 <code>&lt;div onclick&gt;</code> / <code>&lt;span onclick&gt;</code> 的 pattern、改為 <code>&lt;button&gt;</code>。</p>
<h3 id="風險-3pagefind-自身的-aria-實作">風險 3：Pagefind 自身的 ARIA 實作</h3>
<p><strong>位置</strong>：Pagefind 的 <code>&lt;details&gt;&lt;summary&gt;</code> filter blocks。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>&lt;details&gt;</code> / <code>&lt;summary&gt;</code> 是 native element、自帶 expand / collapse、enter 切換</li>
<li>Pagefind 包了 <code>.pagefind-ui__filter-name</code> class 但底層仍是 native — 行為跟著</li>
<li>這是好的設計、不需要動</li>
</ul>
<p><strong>症狀</strong>：rare、native element 多半 OK。</p>
<p><strong>第一個該查的</strong>：確認 Pagefind 沒用 div+role 重新實作這些 — 從 source 看大致符合 native first principle。</p>
<h3 id="風險-4search-input-用-input-typesearch-還是-input-typetext">風險 4：Search input 用 <code>&lt;input type=&quot;search&quot;&gt;</code> 還是 <code>&lt;input type=&quot;text&quot;&gt;</code></h3>
<p><strong>位置</strong>：Pagefind 自身的 input。</p>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>&lt;input type=&quot;search&quot;&gt;</code> 在 mobile 顯示「搜尋」鍵盤、自帶清除按鈕</li>
<li><code>&lt;input type=&quot;text&quot;&gt;</code> 純文字輸入</li>
</ul>
<p><strong>症狀</strong>：mobile 鍵盤不適配搜尋場景、額外清除 UI 自己做。</p>
<p><strong>第一個該查的</strong>：確認 Pagefind 用 <code>type=&quot;search&quot;</code>。從 pagefind-ui 渲染結果可看到 <code>type=&quot;text&quot;</code>、有自訂的清除按鈕 — 可考慮是否值得改。</p>
<hr>
<h2 id="內在屬性比較四種實作-radio-group-的方式">內在屬性比較：四種實作 radio group 的方式</h2>
<table>
  <thead>
      <tr>
          <th>實作</th>
          <th>鍵盤切換</th>
          <th>screen reader 認</th>
          <th>維護成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>&lt;fieldset&gt;&lt;legend&gt;</code> + <code>&lt;input type=&quot;radio&quot;&gt;</code> × N</td>
          <td>是 — HTML 內建</td>
          <td>是 — fieldset semantic</td>
          <td>低</td>
      </tr>
      <tr>
          <td><code>&lt;div role=&quot;radiogroup&quot;&gt;</code> + <code>&lt;input type=&quot;radio&quot;&gt;</code> × N</td>
          <td>是 — input radio 自帶</td>
          <td>部分 — div role 跟 input semantic 重複</td>
          <td>中</td>
      </tr>
      <tr>
          <td><code>&lt;div role=&quot;radiogroup&quot;&gt;</code> + <code>&lt;div role=&quot;radio&quot;&gt;</code> × N</td>
          <td>否 — 要自己寫</td>
          <td>是 — 但需作者完整實作 ARIA pattern</td>
          <td>高</td>
      </tr>
      <tr>
          <td>純自訂無 ARIA</td>
          <td>否</td>
          <td>否</td>
          <td>不適用</td>
      </tr>
  </tbody>
</table>
<p>優先順序：<strong>fieldset &gt; div role + native input &gt; div role + div role</strong>。</p>
<hr>
<h2 id="aria-使用的判斷流程">ARIA 使用的判斷流程</h2>
<p>每個 UI 元素開始實作前、走這個流程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">1. 有沒有 native element 對應？
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   是 → 用 native（fieldset、button、input、details / summary）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   否 → 進 2
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">2. 有沒有 ARIA pattern 對應？
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   是 → 用 div + role + 完整 ARIA 屬性 + 自己寫鍵盤行為
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   否 → 進 3
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">3. 用 div + 自己想 semantic
</span></span><span class="line"><span class="ln">10</span><span class="cl">   注意：可能 screen reader 不認得、需要充分測試</span></span></code></pre></div><p>多數情境停在 1 — native HTML 涵蓋常見 UI 模式。需要走到 2、3 的場景比想像中少。</p>
<hr>
<h2 id="設計取捨實作-ui-元素的策略">設計取捨：實作 UI 元素的策略</h2>
<p>四種做法、各自機會成本不同。這個專案永遠優先選 A（native HTML element）— 不夠用才退到 B / C / D。</p>
<h3 id="a純-native-html-element永遠的首選">A：純 native HTML element（永遠的首選）</h3>
<ul>
<li><strong>機制</strong>：用 <code>&lt;button&gt;</code>、<code>&lt;fieldset&gt;&lt;legend&gt;</code>、<code>&lt;details&gt;&lt;summary&gt;</code>、<code>&lt;input type=&quot;search&quot;&gt;</code> 等 native 元素</li>
<li><strong>選 A 的理由</strong>：semantic + 鍵盤 + focus + form 整合「四件套」自帶、跨瀏覽器一致、跨 screen reader 一致</li>
<li><strong>適合</strong>：所有 native 涵蓋的 UI 模式（按鈕、表單、disclosure、radio group）</li>
<li><strong>代價</strong>：受 native 視覺預設限制、客製樣式可能要對抗 UA 預設</li>
</ul>
<h3 id="bnative--aria-補強aria-label--aria-describedby--aria-expanded">B：Native + ARIA 補強（aria-label / aria-describedby / aria-expanded）</h3>
<ul>
<li><strong>機制</strong>：native element 加 ARIA 屬性補強 semantic 或表達動態狀態</li>
<li><strong>跟 A 的取捨</strong>：B 在 A 的基礎上加細節、不取代</li>
<li><strong>B 比 A 好的情境</strong>：native 已涵蓋主要功能、需要補額外資訊（label、描述）或動態狀態（expanded / pressed / checked）</li>
</ul>
<h3 id="cdiv-rolex--完整-aria-pattern--自寫鍵盤行為">C：<code>&lt;div role=&quot;X&quot;&gt;</code> + 完整 ARIA pattern + 自寫鍵盤行為</h3>
<ul>
<li><strong>機制</strong>：用 div 包成 semantic 元素、加 role + 完整 ARIA + JS 補鍵盤</li>
<li><strong>跟 A 的取捨</strong>：C 給更高客製彈性、A 拿成熟方案；C 維護成本高（要自己保證所有行為）</li>
<li><strong>C 比 A 好的情境</strong>：native 沒對應的 UI 模式（complex tree view、custom slider）— 必須自己定義 semantic</li>
</ul>
<h3 id="d純-div--自訂-semantic無-aria">D：純 div + 自訂 semantic（無 ARIA）</h3>
<ul>
<li><strong>機制</strong>：用 div 自己想 semantic、不加 role</li>
<li><strong>成本特別高的原因</strong>：screen reader 不認得、鍵盤無法操作、違反 a11y 標準</li>
<li><strong>D 是反模式</strong>：違反 a11y 標準（屬合規 / 法規層）— 純視覺裝飾元素（無互動）才能例外</li>
</ul>
<hr>
<h2 id="判讀徵兆">判讀徵兆</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>Refactor 動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用 <code>&lt;div role=&quot;X&quot;&gt;</code> 取代有 native 的 element</td>
          <td>評估改用 native、減少 ARIA 維護</td>
      </tr>
      <tr>
          <td>自訂 UI 鍵盤無法操作</td>
          <td>改用 native button / input、自帶鍵盤行為</td>
      </tr>
      <tr>
          <td>自訂 form 元素跟 form submission 不整合</td>
          <td>改用 native input、自動加入 form data</td>
      </tr>
      <tr>
          <td>Screen reader 不一致地解讀 ARIA</td>
          <td>改用 native、多數 screen reader 對 native 一致</td>
      </tr>
  </tbody>
</table>
<p><strong>核心原則</strong>：ARIA 的 first rule 是「能用 native 就不用 ARIA」。Native element 是 50 年累積的瀏覽器 + 輔助技術知識結晶、不要繞道。</p>
]]></content:encoded></item></channel></rss>