HTML5 網頁語法取代 JavaScript 的五個範例

現在的 HTML5 已經可以靠原生語法做掉以前非寫 JavaScript 不可的功能:摺疊選單用 <details>、表單驗證用 requiredpattern、彈出視窗用 <dialog>、下拉提示用 Popover API、自動完成用 <datalist>。這五個範例合起來,通常可以讓一個典型的行銷頁少載入 jQuery、Popper.js、Select2 這類函式庫,壓縮後省下大約 60 到 100 KB 的傳輸量。更重要的是無障礙行為、鍵盤操作、焦點管理都由瀏覽器內建處理,不必自己重造一次。以下每個範例都附上可以直接貼上去跑的程式碼,以及我認為什麼情況該退回去寫 JS。

先看全貌:五個範例一次比較

先把結論攤開來。這五組語法解決的都是「互動元件」這一類需求,也剛好是過去十幾年網頁設計專案裡最常被外掛塞爆的地方。左邊是你以前寫的東西,右邊是現在可以換掉的寫法。

五個 HTML5 原生語法與被取代對象一覽
需求 過去的做法 HTML5 原生寫法 壓縮後可省下(概估)
摺疊選單、FAQ 手風琴 jQuery slideToggle、Bootstrap Collapse <details> + <summary> + name 約 20 KB
表單欄位檢查 jQuery Validate、自寫 regex 檢查 requiredpatterntype:user-invalid 約 8 KB
彈出視窗、燈箱 Bootstrap Modal、Magnific Popup <dialog> + command / commandfor 約 15 KB
下拉選單、提示泡泡 Popper.js、Tippy.js popover + popovertarget 約 12 KB
輸入自動完成 Select2、typeahead.js、Awesomplete <datalist> + list 約 25 KB

數字是概估值,實際情況看你原本裝了什麼。但重點其實不在那幾十 KB——現在光纖跟 4G 都不缺這點頻寬。真正省下的是維護成本:不用再處理 Esc 鍵關不掉、Tab 鍵跑到背景去、螢幕閱讀器讀不出狀態這些鬼問題。

範例一:<details> 與 <summary> 取代手風琴選單

這大概是投資報酬率最高的一個。以前做一組 FAQ 摺疊,要寫 HTML 結構、寫 CSS 的 max-height 過場、綁 click 事件、還要記得加 aria-expanded。現在四行就結束了。

<details> <summary>運費怎麼算?</summary> <p>訂單滿 1,000 元免運,未滿酌收 80 元。</p> </details>

加上 open 屬性可以讓它預設展開。而 2024 年之後全瀏覽器都支援的 name 屬性,讓它直接變成「一次只能開一個」的手風琴——這是以前一定要寫 JS 才做得到的行為:

<details name="faq"> <summary>可以退換貨嗎?</summary> <p>到貨七天內未拆封可退換。</p> </details> <details name="faq"> <summary>可以開統編嗎?</summary> <p>結帳時填寫統編即可,發票寄送至指定 Email。</p> </details> <details name="faq"> <summary>有實體門市嗎?</summary> <p>台北、台中各一間,營業時間 11:00–20:00。</p> </details>

三個 details 共用同一個 name,展開其中一個,另外兩個自動收起。零 JavaScript。

還有一個很多人不知道的好處:使用者按 Ctrl+F 在頁面上搜尋文字時,瀏覽器會自動展開藏在收合區塊裡的內容並捲到那個位置。用 JS 做的手風琴如果把內容設成 display:none,這個功能就死了。搜尋引擎那邊也一樣,<details> 裡的文字是完整被抓取的,不會因為預設收合就被當成隱藏內容。

唯一要自己補的是外觀。原生三角形箭頭長得很陽春,用 summary::marker::-webkit-details-marker 換掉就好。展開的高度動畫則要靠 interpolate-size: allow-keywords 搭配 transition,目前 Chrome 系列支援得比較完整,Safari 跟 Firefox 上會直接跳出來、沒有滑順過場。如果那個動畫對你的專案是必要的,這裡就是要不要退回 JS 的分界線。

範例二:原生表單驗證取代 jQuery Validate

表單驗證是我看過最常被過度工程化的一塊。一個聯絡表單,五個欄位,結果掛了 jQuery 加 jQuery Validate 加中文語系檔,就為了跳「請輸入正確的 Email」。這些 HTML5 全部內建。

<form> <label for="name">姓名</label> <input id="name" name="name" type="text" required minlength="2"> <label for="email">電子郵件</label> <input id="email" name="email" type="email" required> <label for="phone">手機號碼</label> <input id="phone" name="phone" type="tel" pattern="09[0-9]{8}" title="請輸入 09 開頭的十碼手機號碼" inputmode="numeric" required> <label for="qty">數量</label> <input id="qty" name="qty" type="number" min="1" max="99" step="1"> <button type="submit">送出</button> </form>

幾個實務上的細節值得講清楚:

  • pattern 的正規表示式會自動被前後錨定,所以不用寫 ^$
  • title 屬性的內容會出現在驗證失敗的提示氣泡裡,不寫的話使用者只會看到「請符合所要求的格式」這種沒營養的訊息。
  • inputmode="numeric" 讓手機跳出數字鍵盤,但仍然保留 type="tel" 的語意。這組合對行動裝置的填答完成率有實質幫助。
  • 樣式請用 :user-invalid 而不是 :invalid。差別在於 :invalid 在頁面一載入、使用者根本還沒動手時就把空欄位全部標紅,體驗很糟;:user-invalid 只在使用者互動過或按了送出之後才生效。
input:user-invalid { border-color: #d9534f; }

需要客製錯誤文案時,用 setCustomValidity() 補一點 JS 就行,但底層的驗證邏輯、必填判斷、送出攔截還是交給瀏覽器。這叫做「用 JS 加強」,不是「用 JS 重寫」。

提醒一句:前端驗證只是體驗優化,絕對不能當作安全機制。任何人打開開發者工具刪掉 required,或是直接用 curl 打你的 API,前端這一層就形同不存在。後端驗證一律照做。

範例三:<dialog> 加上 command 屬性做出零 JS 的燈箱

<dialog> 元素本身從 2022 年就全面可用了,但過去大家覺得它不夠香,因為要打開它得呼叫 showModal()——還是得寫 JS。2025 年之後情況變了:Invoker Commands API 的 commandcommandfor 屬性讓按鈕可以直接指名要操作哪個對話框。

<button command="show-modal" commandfor="signup">免費註冊</button> <dialog id="signup"> <h2>建立帳號</h2> <p>填寫 Email 即可開始使用,不需要信用卡。</p> <form method="dialog"> <button>關閉</button> </form> </dialog>

整段沒有一行 JavaScript。而且瀏覽器免費附贈這些行為:

  • 按 Esc 自動關閉。
  • 焦點鎖在對話框內,Tab 不會跑到背景的連結上。
  • 關閉後焦點自動還給觸發它的那顆按鈕。
  • 背景內容自動變成 inert,螢幕閱讀器不會念到。
  • 遮罩層可以用 dialog::backdrop 直接設定樣式,不必自己疊一個半透明的 div。

光是第二點跟第三點,自己用 JS 實作的焦點陷阱就很容易寫壞。我看過太多 modal 一關掉,焦點就掉回頁面最上方,鍵盤使用者要重按二十幾次 Tab 才回得到原位。

<form method="dialog"> 是另一個常被忽略的老語法:表單送出時不會發任何請求,只會關閉對話框,並把按鈕的 value 寫進 dialog.returnValue。做「確定 / 取消」的確認框剛好夠用。

要注意 commandcommandfor 是相對新的東西,各家瀏覽器大約在 2025 年上半年陸續支援。如果你的訪客有一定比例卡在舊版,就補一段五行的 fallback:偵測 'command' in HTMLButtonElement.prototype,不支援時才綁 click 事件呼叫 showModal()

範例四:Popover API 取代 Popper.js 與 Tippy.js

下拉選單、提示泡泡、通知面板——這類「浮在上面、點旁邊會關掉」的東西,以前是 Popper.js 的地盤。現在 HTML 有 popover 屬性。

<button popovertarget="menu">會員中心</button> <div id="menu" popover> <ul> <li><a href="/orders">我的訂單</a></li> <li><a href="/points">紅利點數</a></li> <li><a href="/logout">登出</a></li> </ul> </div>

預設的 popover(等同 popover="auto")自帶「light dismiss」:點外面關掉、按 Esc 關掉、同時只會開一個。這正是下拉選單該有的行為,而且不用你寫任何監聽器。如果要做那種必須按按鈕才關的通知條,改成 popover="manual"

Popover 內容會被放進「top layer」,意思是它永遠疊在最上面,不受父層 overflow: hiddenz-index 堆疊環境影響。做過側邊欄下拉選單被裁切、然後跟 z-index 大戰三百回合的人,應該懂這件事有多值錢。

把元件的行為交還給瀏覽器,你少寫的不是程式碼,是那些你永遠測不完的邊緣情況。

誠實講一個現階段的缺口:定位。要讓泡泡精準貼在按鈕下方,得靠 CSS Anchor Positioning(anchor-nameposition-anchor),而這項功能到目前為止在 Chromium 系列跑得最好,Safari 與 Firefox 的進度落後一截。若你的下拉選單就在按鈕正下方、位置固定,用一般的 position: absolute 搭配相對定位的父層就解決了。若你需要「空間不夠時自動翻到上面」這種聰明行為,跨瀏覽器目前還是 Popper.js 比較穩。

範例五:<datalist> 取代自動完成套件

<datalist> 是這五個裡面資歷最老的,IE10 就有了,卻是最沒人用的一個。它讓輸入框在打字時跳出建議清單,同時又允許使用者輸入清單以外的內容——這正是 <select> 做不到、而大家跑去裝 Select2 的原因。

<label for="city">配送縣市</label> <input id="city" name="city" list="city-list" placeholder="輸入或選擇"> <datalist id="city-list"> <option value="臺北市"> <option value="新北市"> <option value="桃園市"> <option value="臺中市"> <option value="臺南市"> <option value="高雄市"> </datalist>

它也能搭配其他 input 型別使用。例如 type="range"list 會在滑桿上顯示刻度標記,type="color"list 會在色票面板加上預設色,這在做設計工具型介面時滿好用的。

但我要把話講明白:<datalist> 有明確的天花板,別勉強它。

  • 下拉清單的外觀完全不能用 CSS 控制,每家瀏覽器長得不一樣。要品牌一致的視覺就別想了。
  • 比對規則是瀏覽器決定的,沒有模糊搜尋、沒有拼音比對、沒有權重排序。
  • 選項要從 API 動態撈,還是得寫 JS 去塞 <option>
  • 沒有多選、沒有標籤(tag)介面。

所以我的分界很簡單:清單是固定的、數量在一百筆以內、選項不需要圖示或副標,就用 <datalist>縣市、國家、常用標籤、單位名稱都符合。反過來說,商品搜尋、會員名單、需要顯示縮圖跟價格的複合式選項,乖乖用套件。

瀏覽器支援度對照

下表整理各功能大致進入穩定版的時間點。實際規劃時建議還是到 caniuse 或 MDN 的 Baseline 標示確認一次,各家小版號偶爾會有出入。

五個範例的瀏覽器支援概況
功能 Chrome / Edge Safari Firefox 現在能不能直接用
<details> 基本功能 2011 年起 2011 年起 2015 年起 可以,完全沒問題
<details name> 互斥手風琴 120(2023 年底) 17.2 130(2024 年 9 月) 可以,舊版只是變成各自獨立收合
原生表單驗證屬性 很早 很早 很早 可以
:user-invalid 119 16.5 88 可以
<dialog> 37 15.4(2022 年 3 月) 98 可以
command / commandfor 135(2025 年 4 月) 18.4 138 建議加 fallback
Popover API 114(2023 年 5 月) 17 125(2024 年 4 月) 可以
CSS Anchor Positioning 125 尚未完整 尚未完整 還不建議當主力方案
<datalist> 很早 12.1 很早 可以

還有哪些 JS 可以順手砍掉

既然都在整理了,這幾個也一起換掉吧。它們的支援度都很成熟,改動成本幾乎是零。

其他可以用原生語法替換的常見套件
原本用什麼 改用 備註
lazysizes、各種 lazyload 外掛 <img loading="lazy" width height> 務必同時寫上寬高,避免版面位移影響 CLS 分數
JS 產生的進度條 <progress><meter> 前者表示進行中的任務,後者表示區間內的量值,別搞混
錨點平滑捲動的 JS CSS scroll-behavior: smooth 記得搭配 prefers-reduced-motion 尊重使用者設定
自製影片播放器 <video controls playsinline> 只有在需要客製 UI 或 DRM 時才值得裝播放器套件
手動綁定的複製按鈕 <input> 搭配 autocomplete 正確值 填正確的 autocomplete 名稱,瀏覽器自動填入的成功率差很多
Sticky 導覽列的 scroll 事件 CSS position: sticky 不會在捲動時觸發大量 repaint,效能好上一截

什麼情況我還是會乖乖寫 JavaScript

寫到這裡要平衡一下。我不主張「零 JS 才是好網頁」,那是另一種形式的教條。以下幾種狀況,硬要用原生語法反而是自找麻煩:

  1. 需要精準控制動畫時序。<details> 的展開過場跨瀏覽器不一致,如果那個滑順展開是你設計稿的核心體驗,用 JS 加 Web Animations API 會單純得多。
  2. 複合式選單。選項要顯示縮圖、價格、庫存標籤,還要支援多選跟遠端搜尋——這超出 <datalist> 的設計範圍太多。
  3. 需要在關閉前攔截。「還沒存檔,確定要離開嗎」這種確認流程,原生 <dialog> 的 Esc 關閉是攔不住的,要靠 cancel 事件加 preventDefault()
  4. 複雜的跨欄位驗證。「結束日期必須晚於開始日期」「兩次密碼要一致」這種相依邏輯,HTML 屬性表達不出來。
  5. 你的專案已經在用框架。React、Vue 專案裡混用原生 popover 有時會跟框架的狀態管理打架,得評估一下值不值得。

判斷原則其實只有一句話:先用 HTML 做到 80%,剩下 20% 再用 JS 補。而不是一開始就裝一包函式庫把 100% 都自己重寫一遍。

如果只能先換一個,我會從這裡下手

<details> 開始。它改動最小、風險最低、立刻就能刪掉一整塊 JS 跟對應的 CSS,而且幾乎所有網站都有 FAQ 或商品規格這種可摺疊的區塊。做完之後你會發現 Lighthouse 的無障礙分數莫名其妙變高了——因為瀏覽器幫你補了一堆你本來會忘記寫的 ARIA 狀態。

第二個換表單驗證。這一步通常能讓聯絡頁直接擺脫 jQuery 依賴,如果那頁只有表單,整個 <script> 標籤都能刪掉。

<dialog> 跟 Popover 我會放在改版時一起處理,因為它們牽涉到既有樣式的重構,零碎地換反而容易出包。<datalist> 則看場合,遇到固定清單的欄位就順手換掉。

下次接到新案子,動手裝套件之前,先花三分鐘去 MDN 查一下這個需求有沒有原生解法。這幾年網頁標準推進的速度比多數人以為的快,很多我們習以為常「一定要寫 JS」的東西,其實早就寫在規格裡了。