跳到主要內容

表單、焦點與輸入法

為什麼輸入框會失焦、中文輸入法為什麼被打斷、autocomplete 為什麼重要,以及驗證訊息該怎麼寫。

約 7 分鐘 · forms-focus-input.md

1. 輸入框每打一個字就失焦

最常見的三個原因:

一、元件在 render 裡被重新定義

function Page() {
  // 每次 render 都是一個「新的」元件型別 → React 卸載舊的、掛載新的
  const Field = () => <input value={value} onChange={...} />;
  return <Field />;
}

元件定義搬到外面。

二、key 綁到會變的值

{items.map((item, i) => <input key={`${item.name}-${i}`} />)}

item.name 隨著輸入改變 → key 改變 → DOM 節點被換掉 → 焦點沒了。key 要用穩定的 id

三、整份清單被重建

state 更新後整個陣列換新物件,框架沒辦法對應舊節點。用穩定 id,並且只更新變動的那一筆。

2. 中文輸入法(IME)

注音、拼音打到一半時,input 事件會不斷觸發,值是還沒選字的組字狀態。此時做搜尋、驗證、格式化,會把使用者的輸入打斷。

let composing = false;

input.addEventListener('compositionstart', () => (composing = true));
input.addEventListener('compositionend', (event) => {
  composing = false;
  handleInput(event);   // 組字結束才處理
});
input.addEventListener('input', (event) => {
  if (composing) return;
  handleInput(event);
});

需要這個處理的情境:即時搜尋、字數統計、自動格式化(電話、金額)、自動跳格。

這是全世界中日韓使用者都會遇到、而英文開發者不會想到的問題。 用 AI 生成的表單幾乎不會處理它。

3. autocomplete

沒有正確的 autocomplete,密碼管理員不會運作,使用者會開始用弱密碼、重複密碼。

欄位
帳號 / email 登入 username
登入密碼 current-password
註冊 / 更改密碼 new-password
簡訊驗證碼 one-time-code
姓名 name(或 given-name / family-name
電話 tel
地址 street-address postal-code country
信用卡 cc-number cc-exp cc-csc

關掉自動填入用 autocomplete="off",但不要用在密碼欄位 —— 瀏覽器多半會忽略它,而你只是讓密碼管理員混亂。

4. 標籤

<!-- 對 -->
<label for="email">電子郵件</label>
<input id="email" type="email" />

<!-- 也對 -->
<label>電子郵件 <input type="email" /></label>

<!-- 錯:placeholder 不是標籤 -->
<input type="email" placeholder="電子郵件" />

用 placeholder 當標籤的三個問題:打字後標籤消失、對比通常不足、螢幕閱讀器不一定會讀。

視覺上不想顯示標籤時,用 .ds-sr 而不是刪掉它。

5. 說明與錯誤要「連結」到欄位

<label class="ds-label" for="pw">密碼</label>
<input class="ds-input" id="pw" type="password"
       aria-describedby="pw-hint" aria-invalid="false" />
<p class="ds-hint" id="pw-hint">至少 10 個字元,包含一個數字。</p>

aria-describedby 讓螢幕閱讀器在唸欄位時一起唸出說明。只是把文字放在旁邊,看得到的人才知道。

出錯時把 aria-describedby 指向錯誤訊息,並設 aria-invalid="true"

6. 驗證訊息怎麼寫

規則:說明哪裡錯 + 怎麼改

✗ 無效的電子郵件
✗ 格式錯誤
✓ 這個電子郵件看起來不完整,要有 @ 和網域,例如 yijun@example.com

其他原則:

  • 要求寫在前面,不要靠失敗來揭露。 密碼規則在欄位下方先講,不要等按下送出才說。
  • 不要在打字時就報錯。 使用者才輸入 y 就說「不是有效的 email」很煩。在 blur 時驗證,送出時再驗一次。
  • 改好了要立刻消失。 修正後仍然報錯,使用者會以為自己還是錯的。
  • 長表單要有頂部總結,列出所有錯誤並可點擊跳到該欄位。

7. 焦點管理

情境 該做的事
開啟彈窗 焦點移到彈窗內第一個可聚焦元素,並鎖在裡面
關閉彈窗 焦點回到觸發它的按鈕
刪除清單項目 焦點移到下一項(或上一項),不要掉回 body
換頁 焦點移到 <main>tabindex="-1")或 h1
表單送出失敗 焦點移到第一個錯誤欄位

原生 <dialog> + showModal() 免費提供焦點鎖定、Escape 關閉、背後 inert。自己刻的 modal 這四樣通常都缺。

8. 驗證碼輸入框

六格 OTP 是焦點管理的考題,四個必做:

  1. inputmode="numeric" 開數字鍵盤,但不要用 type="number"(會有上下箭頭、貼上行為怪異)
  2. autocomplete="one-time-code" 讓 iOS / Android 提供簡訊驗證碼
  3. 貼上一整串六位數字時,要自動分配到六格。只處理逐字輸入的版本,使用者一貼上就只會填進一格
  4. 空格按 Backspace 要跳回前一格
function onInput(index: number, event: Event) {
  const value = (event.target as HTMLInputElement).value.replace(/\D/g, '');

  if (value.length > 1) {
    // 貼上或自動填入:把它攤開
    const chars = value.slice(0, LENGTH - index).split('');
    digits = digits.map((d, i) =>
      i >= index && i < index + chars.length ? chars[i - index] : d
    );
    focusBox(index + chars.length);
    return;
  }

  digits[index] = value;
  if (value) focusBox(index + 1);
}

參考模組:驗證碼輸入

9. 行動裝置

  • iOS 會在輸入框字體小於 16px 時自動放大整個頁面。輸入框字體 ≥ 16px,或設 maximum-scale(但那會傷害無障礙,寧可調字體)
  • inputmode 決定鍵盤類型:numeric decimal tel email url search
  • enterkeyhint 決定 Enter 鍵的樣子:search send next done
  • 表單裡的按鈕記得寫 type="button",否則預設是 submit,按下去會送出整個表單

10. 檢查清單

  • 每個輸入框都有真正的 <label>
  • 說明與錯誤用 aria-describedby 連結
  • autocomplete 都填了
  • 有處理 IME 組字事件
  • 錯誤訊息說得出「怎麼改」
  • 不在打字時報錯,修好會立刻消失
  • 彈窗開關的焦點有進有回
  • 輸入框字體 ≥ 16px
  • 非送出按鈕都有 type="button"

顯示設定

這裡改的每一項,會即時套用到站上所有預覽。

風格

密度

圓角

動態

系統層級的「減少動態效果」永遠優先於這裡的設定。

語言