在日常專案開發中,限制使用者輸入格式是最基礎的表單需求。許多剛接觸前端的工程師看到 HTML input type 提供了
number、url、tel與date等語意化類型,便以為只要宣告對應的type屬性就能完成格式把關。最近在重構金流結帳與會員註冊表單時,正好排查了多起因過度依賴原生type屬性而引發的線上資料異常(例如type="number"被滑鼠滾輪偷偷改動金額、輸入12e導致input.value變成空字串、或是type="url"放行非 HTTP 協定字串)。這篇筆記將從 WHATWG HTML Standard §4.10.5 (<input>element) 底層規範出發,完整拆解各類型input type的真實行為邊界與解法。
⚡ 快速簡單使用(30 秒複製即用範本)
在設計表單時,請先區分欄位是「可加減計算的量化數值」還是「由數字組成的識別碼字串」,並針對原生驗證盲點加上對應防禦:
<form id="checkout-form" novalidate>
<!-- 情境 A:可加減計算的量化數值(購買數量)➔ 使用 type="number" 並攔截滾輪誤觸 -->
<div class="form-field">
<label for="order-qty">購買數量(1 ~ 99 件)</label>
<input
type="number"
id="order-qty"
name="quantity"
min="1"
max="99"
step="1"
required
>
</div>
<!-- 情境 B:不可加減的數字識別碼(信用卡號、郵遞區號、驗證碼)➔ 使用 type="text" 保留前導零與 maxlength -->
<div class="form-field">
<label for="postal-code">郵遞區號(3 碼、3+2 共 5 碼或 3+3 共 6 碼數字)</label>
<input
type="text"
id="postal-code"
name="postalCode"
inputmode="numeric"
pattern="[0-9]{3}([0-9]{2,3})?"
maxlength="6"
autocomplete="postal-code"
required
>
</div>
<!-- 情境 C:嚴格限制 HTTPS/HTTP 協定之網址欄位 -->
<div class="form-field">
<label for="webhook-url">Webhook 通知網址(限 https://)</label>
<input
type="url"
id="webhook-url"
name="webhookUrl"
pattern="https://.*"
placeholder="https://api.example.com/webhook"
required
>
</div>
<button type="submit">確認送出</button>
</form>
<script>
// 防護 1:防止使用者捲動頁面時,滑鼠滾輪意外改變聚焦中的 type="number" 數值
document.addEventListener('wheel', (event) => {
const active = document.activeElement;
if (active && active.tagName === 'INPUT' && active.type === 'number') {
active.blur();
}
}, { passive: true });
</script>
一、核心速查:HTML input type 原生行為與隱藏限制對照表
在深入底層規範前,我們先透過以下速查表盤點 6 種最常被誤用的 HTML input type 原生限制與工程應對策略:
input type 屬性 | 開發者常見誤解 | WHATWG 規範實際原生行為 | 解法與替代方案 |
|---|---|---|---|
type="number" | 以為只能輸入 0-9 整數,且可用 maxlength 限制長度 | 允許輸入科學記號 e、E、+、- 與小數點;完全忽略 maxlength 與 pattern 屬性;輸入無效數值時 input.value 會回傳空字串 "" | 僅用於「數量、年齡」等可計算數值;搭配 validity.badInput 檢核與滾輪防護。若為卡號、郵遞區號或驗證碼,改用 type="text" inputmode="numeric" |
type="email" | 以為會嚴格驗證真實頂級網域(TLD) | 依 WHATWG §4.10.5.1.5 允許無點號網域(如 user@localhost、test@com);規範建議瀏覽器在 value 中以 Punycode(xn--...)保存國際化網域,Chrome 即如此處理 | 前端保留 type="email" 以喚起 @ 專用鍵盤,提交時結合嚴格正規表達式與後端 DNS / 信箱驗證 |
type="url" | 以為只會放行 http:// 或 https:// 網址 | 依 WHATWG §4.10.5.1.4 僅檢查值是否為合法的絕對 URL(valid absolute URL),因此 mailto:a@b、ftp://files 甚至 javascript:void(0) 皆能通過原生驗證 | 搭配 pattern="https?://.+" 或使用 JavaScript new URL(val) 明確驗證 url.protocol === 'https:' |
type="tel" | 以為瀏覽器會自動檢查電話號碼格式與字元 | 依 WHATWG §4.10.5.1.3 完全不執行任何格式驗證(僅剔除換行符號 \r\n),允許輸入任意中英文字元 | 定位為「行動端電話鍵盤觸發器」,必須額外搭配 pattern 或前端格式化函式與後端檢核 |
type="date" | 以為 input.valueAsDate 取出的時間是使用者本地時區 00:00:00 | value 格式固定為 YYYY-MM-DD,但 valueAsDate 回傳的是 UTC 午夜 (00:00:00 UTC),跨時區轉換極易產生日期偏移 | 讀取日期字串時優先解析 input.value(YYYY-MM-DD),避免直接對 valueAsDate 呼叫本地時區方法 |
type="search" | 以為與 type="text" 行為 100% 相同 | 值淨化演算法會移除所有 \r 與 \n 換行字元,且在 WebKit / Blink 瀏覽器會自動渲染原生清除叉叉按鈕 (::-webkit-search-cancel-button) | 若需自訂清除按鈕圖示,需透過 CSS 隱藏瀏覽器預設 -webkit-search-cancel-button 偽元素 |

二、深度拆解:type="number" 的五大原生限制與 GOV.UK 實作要點
在所有表單類型中,type="number" 是引發最多生產環境問題的屬性。英國政府數位服務團隊(GOV.UK Design System)甚至為此發表了專文〈Why the GOV.UK Design System team changed the input type for numbers〉,將日期輸入元件與銀行帳戶資料等相關模式中的 type="number" 改為 type="text" inputmode="numeric" pattern="[0-9]*",文中提到的原因包含滑鼠滾輪誤改數值,以及 NVDA、Dragon NaturallySpeaking 等輔助科技的相容問題。以下拆解 type="number" 的五大原生限制:
1. 值淨化演算法與無效輸入規則導致 input.value 回傳空字串
根據 WHATWG HTML Standard §4.10.5.1.12 (Number state),<input type="number"> 的值必須是合法的浮點數(Valid floating-point number),因此瀏覽器允許使用者在欄位中輸入科學記號 e、E、正負號 +、- 與小數點 .。
更關鍵的是規範對 value 的兩道限制:瀏覽器不得讓使用者把 value 設成「非空但不是合法浮點數」的字串;透過程式設定非法值時,值淨化演算法(Value Sanitization Algorithm)也會把它改為空字串。因此當使用者在輸入框打入不完整的科學記號(例如 12e 或 10+-2)時,雖然畫面上依然看得到 12e,input.value 卻是空字串 ""(同時 input.valueAsNumber 為 NaN,validity.badInput 為 true;若欄位設有 required,validity.valueMissing 也會是 true)!

type="number" 輸入無效浮點數 12e 時,input.value 回傳空字串與 validity.badInput 之判定差異如果前端驗證程式只寫了 if (input.value === ''),就會把「格式輸入錯誤(12e)」誤判為「使用者尚未填寫此欄位」,向使用者顯示令人困惑的「此欄位為必填」提示!解決思路是先檢查 ValidityState.badInput:
function validateNumberInput(inputEl) {
// 1. 優先檢查是否有無法解析為浮點數之無效輸入(如 "12e")
if (inputEl.validity.badInput) {
return { valid: false, message: '請輸入有效的純數字格式(不含科學記號或特殊符號)。' };
}
// 2. 再檢查是否真的未填寫
if (inputEl.validity.valueMissing || inputEl.value.trim() === '') {
return { valid: false, message: '此數量欄位為必填。' };
}
// 3. 檢查上下限與步進值
if (inputEl.validity.rangeUnderflow || inputEl.validity.rangeOverflow) {
return { valid: false, message: `數值必須介於 ${inputEl.min} 到 ${inputEl.max} 之間。` };
}
if (inputEl.validity.stepMismatch) {
return { valid: false, message: '請輸入整數數量。' };
}
return { valid: true, value: inputEl.valueAsNumber };
}
2. 原生完全忽略 maxlength 與 pattern 屬性
根據 WHATWG 規範 <input> 各 type 狀態的屬性適用摘要表(bookkeeping details),maxlength、minlength 與 pattern 屬性僅適用於文字類狀態(text、search、url、tel、email、password)。在 <input type="number"> 上設定 maxlength="6" 或 pattern="[0-9]*" 會被瀏覽器完全忽略,毫無作用。
3. 呼叫游標選取 API (setSelectionRange) 會直接拋出 DOMException
當我們實作自動格式化或驗證錯誤自動反白功能時,常會存取 input.selectionStart 或呼叫 input.setSelectionRange(0, 2)。然而根據 WHATWG §4.10.20 APIs for the text control selections,type="number" 與 type="email"、type="date" 均不支援文字選取範圍 API:讀取 selectionStart 只會得到 null,呼叫 setSelectionRange() 則會拋出 InvalidStateError 的 DOMException。以 Chrome 為例,錯誤訊息為:
InvalidStateError: Failed to execute 'setSelectionRange' on 'HTMLInputElement': The input element's type ('number') does not support selection.
4. 自動吃掉前導零(Leading Zeros)與大數 IEEE 754 精度流失
type="number" 將輸入視為數學數值而非字元序列:
- 若用於台灣手機號碼(
0912345678)、郵遞區號(001)或銀行代碼(004),轉為數值處理或步進時會抹除開頭的0。 - 若用於 16 位數信用卡號或超長訂單編號,一旦超過 JavaScript
Number.MAX_SAFE_INTEGER(2^53 - 1,即9007199254740991),input.valueAsNumber就會發生 IEEE 754 雙精度浮點數進位失真。
5. 滑鼠滾輪(Scroll Wheel)誤觸改值與螢幕閱讀器體驗問題
在 Chrome 與 Edge 等桌面瀏覽器中,當使用者點擊 <input type="number"> 輸入完數字後,若焦點仍停留在輸入框內,此時使用者轉動滑鼠滾輪想往下捲動網頁點擊送出按鈕,瀏覽器卻會攔截 wheel 事件並將輸入框內的數字悄悄遞增或遞減!此外,依 HTML-AAM 對應規則,type="number" 在無障礙樹中的角色是 spinbutton(Chrome 即如此對應),螢幕閱讀器會將其播報為微調按鈕,若欄位填寫的是身分證號或信用卡號,會讓視障使用者誤以為該數值需要上下微調。
三、type="email"、type="url" 與 type="date" 的規範邊界
除了 type="number" 之外,其他語意化輸入型態也有必須注意的規範邊界:
1. type="email":故意放寬的 RFC 5322 規則與 Punycode 轉換
許多工程師疑惑為什麼 user@localhost 或 admin@company 這種沒有 .com 頂級網域的字串能通過 <input type="email"> 驗證?
在 WHATWG HTML Standard §4.10.5.1.5 中特別註明了,這個 email 語法定義是對 RFC 5322 的「刻意違反(willful violation)」,理由是 RFC 5322 的語法對實際表單使用來說同時過嚴、過於模糊又過於寬鬆;規範定義的網域部分只要求由合法標籤組成,並不要求一定要有點號。但在一般對外商業網站中,這會導致使用者漏打 .com(例如把 [email protected] 打成 user@gmail)時直接通過前端檢核。
此外,當使用者輸入包含非 ASCII 字元的國際化網域信箱(IDN,例如 @ 後方包含多國語系網域字元)時,規範建議瀏覽器在畫面上顯示 IDN、在 value 中保存 Punycode;以 Chrome 為例,輸入 user@中文.tw 後,input.value 會是 [email protected]。
2. type="url":只驗絕對 URL 語法,不限制 HTTP/HTTPS 協定
依 WHATWG HTML Standard §4.10.5.1.4 (URL state),<input type="url"> 的原生驗證只檢查值是否為 WHATWG URL Standard 定義的合法絕對 URL,不限定 scheme。這意味著:
http://localhost(通過)ftp://192.168.1.1/file.zip(通過)mailto:[email protected](通過)custom-app://open(通過)
若你的業務邏輯是讓使用者填寫「個人網站」或「Webhook 回呼網址」,並將該網址渲染至前台 <a href="..."> 中,若未限制協定必須為 https: 或 http:,不僅會產生無效連結,更可能引發資安風險。
3. type="date":valueAsDate 的 UTC 時區偏移陷阱
當使用者在 <input type="date"> 選擇 2026-10-30 時:
input.value永遠回傳純字串"2026-10-30"。input.valueAsDate則會回傳一個指向2026-10-30T00:00:00.000Z(UTC 零時) 的 JavaScriptDate物件。若使用者的電腦位於西半球時區(如美東UTC-4),直接對input.valueAsDate.getDate()取值會得到前一天(29號)!因此實務上處理日期欄位時,建議直接拆分input.value字串或明確指定時區。
四、實作防禦架構:結合 Constraint Validation API 與語意化標籤
為了兼顧「行動裝置最佳虛擬鍵盤體驗」與「嚴謹的商業邏輯驗證」,現代前端表單應採用 HTML5 Constraint Validation API 搭配明確的 <label for> 可標籤化元素綁定規範。
以下示範如何針對「數量(type="number")」、「Email(type="email")」與「網站網址(type="url")」撰寫精準的原生 + 自訂雙重驗證器:
<form id="profile-form" novalidate>
<!-- 1. 電子信箱:保留 type="email" 喚起專用鍵盤,並補上點號網域檢核 -->
<div class="field-group">
<label for="work-email">工作電子信箱</label>
<input
type="email"
id="work-email"
name="workEmail"
autocomplete="email"
autocapitalize="none"
spellcheck="false"
required
>
<p id="work-email-err" class="field-error" hidden></p>
</div>
<!-- 2. 個人網站:使用 type="url" 搭配 URL 建構子檢核 https: 協定 -->
<div class="field-group">
<label for="portfolio-url">個人作品集網址(須以 https:// 開頭)</label>
<input
type="url"
id="portfolio-url"
name="portfolioUrl"
placeholder="https://example.com"
required
>
<p id="portfolio-url-err" class="field-error" hidden></p>
</div>
<button type="submit">儲存設定</button>
</form>
<script>
const form = document.getElementById('profile-form');
const emailInput = document.getElementById('work-email');
const urlInput = document.getElementById('portfolio-url');
function validateEmailField(el) {
el.setCustomValidity(''); // 先重置自訂錯誤狀態
if (el.validity.valueMissing) {
el.setCustomValidity('請輸入工作電子信箱。');
} else if (el.validity.typeMismatch) {
el.setCustomValidity('電子信箱格式不正確,請確認包含 @ 符號。');
} else {
// 補強 WHATWG 允許無點號網域 (user@com) 之限制
const domainPart = el.value.split('@')[1] || '';
if (!domainPart.includes('.') || domainPart.endsWith('.')) {
el.setCustomValidity('請輸入包含完整網域名稱的信箱(例如 example.com)。');
}
}
const errEl = document.getElementById(`${el.id}-err`);
errEl.textContent = el.validationMessage;
errEl.hidden = el.validity.valid;
return el.validity.valid;
}
function validateHttpsUrlField(el) {
el.setCustomValidity('');
if (el.validity.valueMissing) {
el.setCustomValidity('請輸入作品集網址。');
} else {
try {
const parsed = new URL(el.value);
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
el.setCustomValidity('網址協定僅支援 https:// 或 http://。');
} else if (!parsed.hostname.includes('.')) {
el.setCustomValidity('請輸入有效的公開網域名稱。');
}
} catch {
el.setCustomValidity('請輸入包含 https:// 的完整網址。');
}
}
const errEl = document.getElementById(`${el.id}-err`);
errEl.textContent = el.validationMessage;
errEl.hidden = el.validity.valid;
return el.validity.valid;
}
form.addEventListener('submit', (e) => {
const isEmailOk = validateEmailField(emailInput);
const isUrlOk = validateHttpsUrlField(urlInput);
if (!isEmailOk || !isUrlOk) {
e.preventDefault();
}
});
</script>
五、常見錯誤與踩坑排查 (Pitfalls & Troubleshooting)
- 呼叫
setCustomValidity('錯誤訊息')後忘記清空,導致表單永遠無法送出:
根據 MDNHTMLInputElement.setCustomValidity()說明,只要傳入非空字串,input.validity.customError就會維持true(checkValidity()永遠回傳false)。因此在每次重新驗證開頭,必須先執行el.setCustomValidity('')將錯誤狀態歸零,再執行後續判斷。 - 未在
<form>加上novalidate導致自訂錯誤 UI 與瀏覽器原生氣泡衝突:
若團隊設計了專屬的欄位下方紅字錯誤提示,請務必在<form>加上novalidate屬性停用瀏覽器預設的提交攔截氣泡,改於submit事件中主動呼叫checkValidity()控制焦點與錯誤訊息顯示。 - 誤以為前端
type與pattern可以取代後端 API 驗證:
所有 HTML 屬性與客戶端 JavaScript 驗證的目的皆是「即時回饋、改善使用者體驗(UX)」;任何請求皆可透過curl或 DevTools 直接繞過瀏覽器發送,因此後端 API 永遠必須執行獨立的 Schema 與型別檢核。
六、總結
理解 HTML input type 的底層規範邊界,能幫助我們在開發表單時做出正確的技術選型:
- 量化數值(數量、年齡):使用
<input type="number">,並防範validity.badInput空字串誤判與滑鼠滾輪誤觸。 - 數字識別碼(信用卡、郵遞區號、OTP 驗證碼、電話):捨棄
type="number",改用<input type="text" inputmode="numeric">或<input type="tel">以保留前導零並支援maxlength。 - Email 與 URL:保留語意化
type="email"與type="url"觸發行動端專屬鍵盤,並透過 Constraint Validation API 補強網域點號與https:協定檢核。
參考資料與延伸閱讀
- 中華郵政:郵遞區號查詢(3+3 郵遞區號)
- 中華郵政客服專區:郵遞區號(3 碼及 3+2 碼維持現行使用方式)
- WHATWG HTML Standard §4.10.5 — The
inputelement & States of thetypeattribute - WHATWG HTML Standard §4.10.5.1.12 — Number state (
type=number) & Value sanitization algorithm - WHATWG HTML Standard §4.10.20 — APIs for the text control selections
- W3C HTML-AAM —
input(type attribute in the Number state) - GOV.UK Technology Blog — Why the GOV.UK Design System team changed the input type for numbers
- MDN Web Docs —
<input>: The HTML Input element - MDN Web Docs — Constraint validation
- MDN Web Docs —
ValidityState.badInputproperty - MDN Web Docs —
HTMLInputElement.setCustomValidity() - W3C WAI — Forms Tutorial: Validating Input
- 延伸閱讀:前端資料儲存 Cookie、localStorage、sessionStorage 使用場景與限制比較
- 延伸閱讀:HTTPOnly Cookie 怎麼設定?新手工程師常見 XSS 防禦實作
發佈留言