請不要因為文章的標題而對我心懷敵意。對於 javascript,我只有喜愛沒有討厭,與此同時,我每天還會編寫大量的 javascript 代碼。不過,除了 javascript 之外,css 和 html 也都是我非常喜愛的技術。我之所以偏愛這三種技術是有些原因的。
web 開發的第一核心原則就是“最小能力原則”。換而言之,也就是當我們實現目標時,應該 選擇相對較輕量的編程技術去實現。
依據此原則,也就意味着在 web 開發中,對於相同功能的實現,開發者應該優先考慮 html,其次是 css,最後才是 js。由於 js 使開發者可以控制瀏覽器的行為,所以 js 在三種技術中使用的最廣泛。不過,js 也會因為諸如外部資源加載失敗、解析異常、執行錯誤等原因,從而導致其對瀏覽器的控制失效。此外,js 對諸如依賴鍵盤、依賴額外輔助設備等對可訪問性有要求的用戶的支持也不夠友好。
與命令式的 js 不同,html 和 css 是聲明式的。因此與使用 js 的情況不同,在使用 html 和 css 時,開發者是告訴瀏覽器做什麼,而不是怎麼做。這也就意味着瀏覽器可以自己選擇怎麼做,從而以最高效的方式實現。
由於 html 和 css 的各項功能都是瀏覽器原生支持的,因此這些功能通常會有更好的性能、更原生、具備更好的用戶體驗,對可訪問性的支持也更好。雖然並非所有的場景都優於 js(特別是在可訪問性方面),但大多數時候,利用瀏覽器原生功能來實現複雜邏輯,會給用戶帶來更好的使用體驗。
也許你會想:“我用 js 實現的所有功能,都需要 js 才能實現。”這種想法可能是對的,但值得注意的是,無論是瀏覽器製造商還是規範編寫者,他們已經將許多功能轉移到了 css 和 html 上,這些功能在幾年前還必須使用 js 才能實現。這就是本文要討論的內容。
web 開發中的一個棘手之處在於,一旦你學會了如何構建某個東西,就再也沒有必要重新再學一次。因為這是 web 開發行業達成的常識約定:web 是向後兼容的(雖然有極少數例外情況,但第一個網頁在當前所有現代瀏覽器上仍然可以正常運行)。
這也就意味着,你所學到的實現方案將會成為你工具箱中的一部分,你可以一直重複實現它,並且每次都能正常運行。所以我接下來會給出一些比較酷的示例,希望文章中的這些示例讓你能明白,那些當初你認為必須通過 javascript 才能解決的問題,其實並不一定現在仍然需要(這也是我為什麼列舉這些示例的原因)。所以,如果你能時不時嘗試這些設想,或許你能開發出更好的網站。
自定義開關,是所有 web 開發人員都不可規避的一個功能,那麼我們就從如何實現一個自定義開關來開始本文。設計師的需求是實現一個漂亮的開關功能而不是一個複選框。與使用 div、onclick 事件處理程序和內部狀態的 js 解決方案不同,這裡我們將通過普通的 checkbox 和:checked 偽類來實現。下面是我們將要用到的 html 代碼:
<label> <input type="checkbox" /> my awesome feature</label>
在上面的代碼中有一個 label 元素,裡面包含一個複選框。這樣做的好處是瀏覽器會默認為我們做一些事情。由於輸入框位於 label 標籤的內部,因此瀏覽器會將它們關聯起來,所以現在我們在 label 標籤的任何位置進行點擊,都能操作複選框的切換,而無需任何 onclick 事件的處理程序。瀏覽器免費為我們實現了這個功能。如果單單就功能而言,我們就已經完成了。
當然,上面這種外觀,設計師可能不喜歡,所以我們需要創建一個外觀開看起來還不錯的自定義開關。那麼接下來,讓我們給自定義開關添加一些 css:
input { appearance: none; position: relative; display: inline-block; background: lightgrey; height: 1.65rem; width: 2.75rem; vertical-align: middle; border-radius: 2rem; box-shadow: 0px 1px 3px #0003 inset; transition: 0.25s linear background;}input::before { content: ""; display: block; width: 1.25rem; height: 1.25rem; background: #fff; border-radius: 1.2rem; position: absolute; top: 0.2rem; left: 0.2rem; box-shadow: 0px 1px 3px #0003; transition: 0.25s linear transform; transform: translatex(0rem);}
上面樣式的具體細節並不重要,但我希望你注意第一條樣式規則:appearance: none。
表單元素和圖像元素都屬於一種被稱為“替換內容”的元素。這意味着,在 html 中這些元素的內容是由瀏覽器提供的,而非標籤本身。在瀏覽器渲染 html 過程中,當發現替換內容時,它會為其留下一個盒子,然後會用實際內容來替換該盒子。這就是為什麼諸如圖像、表單等 “替換內容”元素不能有偽元素的原因:當瀏覽器把整個元素替換掉時,這些偽元素也會同時被替換掉。
給元素設置 appearance,是禁止瀏覽器這種行為的方式之一。它告訴瀏覽器:“謝謝,但我想自定義我的表單控件樣式”。這樣我們就可以使用::before 偽元素了。現在輸入框本身就是我們開關的背景,而::before 偽元素就是其中負責切換功能的小圓點。
點擊仍然會觸發複選框選中和未選中的切換,但由於我們替換了元素,所以選中和未選中的狀態展示需要我們自己處理。因此我們引入:checked 偽類來實現此功能:
:checked { background: green;}:checked::before { transform: translatex(1rem);}
當你點擊複選框時,:checked 偽類開始匹配,從而實現樣式的更新。
到目前為止,通過原生的 html 元素和一些 css 我們創建了一個外觀漂亮的自定義開關,但事情還沒完。因為,對於使用鼠標的用戶來說,可以很明顯的知道他們正在與哪個表單控件進行交互(因為他們可以指向並點擊),而對於使用鍵盤的用戶來說,情況就不那麼容易了。
譯註:下圖是使用鍵盤操作的時候出來的樣式:
我相信你對下面這段 css 代碼會很熟悉。這段 css 代碼是為了將那個醜陋的、虛線的、方形的外邊框去掉。
input:focus { outline: none;}
如果你正在閱讀這篇文章,那麼後面會知道這不是一個好的實現方式。但是我們如何才能讓它看起來更好呢?在這方面,瀏覽器也進行了更新,為我們提供了更好的體驗。現在元素邊框的圓角設置同樣會在外邊框上生效,並且我們還可以將其偏移或嵌入到元素內部:
input:focus-visible { outline: 2px solid dodgerblue; outline-offset: 2px;}
現在,當用戶使用鍵盤與元素進行交互(你可以嘗試在點擊後按下空格鍵,或者使用 tab 鍵切換到它),:focus-visible 會生效(使用鼠標時不會),並且它們會在元素周圍顯示一個好看的、藍色的輪廓線。
最後,我希望你將上面那個 outline: none 替換為如下內容:
input:focus { outline-color: transparent;}
兩個 css 代碼都將產生相同的效果:這裡是通過將外邊框顏色設置為透明,而不是通過隱藏外邊框來實現的。這樣做的好處是,對於打開了高對比度模式(也稱為強制顏色)的用戶而言,此時外邊框會再次顯示出來,因為在高對比度模式下,透明顏色會被用戶所選擇的顏色替代,從而幫助他們看清楚正在與之交互的內容,即使他們使用鼠標也一樣有效果。
本文篇幅不足以詳細介紹強制顏色的功能,但如果你想了解更多,請查看我的文章 《強制顏色解析》(https://polypane.app/blog/forced-colors-explained-a-practical-guide/)。
<input list="frameworks" /><datalist id="frameworks"> <option>bootstrap</option> <option>tailwind css</option> <option>foundation</option> <option>bulma</option> <option>skeleton</option></datalist>
你可以通過在 html 中添加一個帶有 id 屬性和一組選項值的 datalist 元素來使用它。不用擔心,該元素默認是不可見的。然後,你需要在輸入框上通過設置 list 屬性來將兩者關聯起來。使用效果如下:
當用戶在輸入框中輸入時,瀏覽器會將 datalist 顯示為下拉列表,並根據用戶輸入自動過濾選項。由於它本質上依然是一個普通的輸入框,所以用戶仍然可以輸入自定義值。最後,用戶可以通過選擇輸入框來查看數據列表,並通過箭頭建進行數據導航。當然,用戶也可以通過點擊瀏覽器默認添加的下拉圖標來查看所有的數據選項。
市面上有很多外觀漂亮的顏色選擇器,這些顏色選擇器除了擁有漂亮的畫布界面之外,還有通過上百行 js 代碼實現的滑塊功能。但是你知道嗎?其實你可以使用瀏覽器原生的顏色選擇器。
<label> <input type="color" /> color </label>
這一行 html 代碼就可以給你一個帶有漂亮界面的顏色選擇器,這就已經節省了大量的 javascript 代碼。除此之外,因為我們將實現讓給了瀏覽器來處理,所以實際上我們還可以免費獲得更多功能。在 chromium 瀏覽器中,這個原生的顏色選擇器還可以讓你從屏幕上的任何位置選擇顏色,這真的是太棒了!
不過這裡值得注意的一點是,雖然瀏覽器顯示了一個漂亮的顏色選擇器,但不一定你的所有用戶都可以使用它。因此提供其他選擇顏色的方式(比如常規文本輸入)仍然是一個好的選擇。
accordions 是一種很好的幫助我們更好地組織頁面上的大量內容,使其結構更清晰,避免展示雜亂無章的方式。而瀏覽器也已經為我們提供了免費的實現方式,開發者通過使用 details 和 summary 元素來實現:
<details> <summary>my accordion</summary> <p>my accordion content</p></details>
默認情況下,details 元素中除了 summary 元素之外,其餘元素內容都是隱藏的。當用戶點擊 summary 元素時,瀏覽器才會顯示其餘的內容。
通常情況下,在一組 accordions 中,會有一個默認處於展開狀態,而其他的則默認處於收起狀態。這一點,你可以通過使用 open 屬性來實現:
<details open> <summary>my accordion</summary> <p>my accordion content</p></details>
如果你是 react 使用者,可能會認為:“太好了,現在它有了 open 屬性,就再也不會收起了。”但還好並非如此。open 屬性只是初始狀態,當用戶與 accordions 進行交互時,它會根據用戶的操作自動進行更新。
在樣式上,details 元素也為我們提供了解決方案。那個小三角形(一旦設計師看到它,就會立刻想要替換掉)是一個偽元素::marker,你可以對其進行樣式設置:
summary::marker { font-size: 1.5em; content: "";}[open] summary::marker { font-size: 1.5em; content: "";}
但值得注意的是,更改內容可能會影響輔助技術對你的 accordions 的解析。你可以閱讀 manuels 的文章 《details/summary 的一致性問題》(https://www.matuzo.at/blog/2023/details-summary) 來了解更多。此外,對於 safari 瀏覽器,你需要使用::-webkit-details-marker 偽元素來處理瀏覽器差異性問題。
偽元素::marker 的樣式設置選項相對有限(許多 css 屬性對其不起作用,例如無法將其完全定位到不同的位置)。但你可以替換其內容,例如使用表情符號,或設置背景顏色或圖像,並更改其字體大小。
通過 open 屬性,你可以輕鬆地分別為展開狀態和收起狀態設置不同的樣式。
最後,我們需要對 summary 元素進行一些處理。雖然它是可點擊的,但其與 a 鏈接元素和按鈕元素都不同;當鼠標懸停的時候,它既沒有指針光標,也沒有類似按鈕那樣的 hove 態。因此,我認為我們應該為它添加鼠標懸停和焦點狀態,以幫助訪問者可以意識到它是可點擊的:
summary:hover,summary:focus { cursor: pointer; background: deeppink;}
在這裡,我不想進行“只有鏈接才應該具有指針光標”的討論,我的主要觀點是你需要做一些處理來提醒用戶。
有時候,你需要向用戶展示一些信息,或者詢問他們問題,或者讓他們確認某些事情。在 javascript 中,這就是 alert()、prompt() 和 confirm() 的作用。但它們有一個很大的問題:它們會鎖定主線程。這意味着你的頁面無法做其他任何事情。而且它們是瀏覽器原生的,無法根據你的設計進行樣式定製化。
如果你自己構建一個對話框,也會遇到一些麻煩:為了支持可訪問性,你需要確保對話框內部能獲得焦點,此外還需要聲明它是模態對話框,以確保用戶無法意外關閉它,最後還要與可能設置了 z-index 為 2147483647 的聊天小部件進行鬥爭(如果你知道這些小部件的存在的話)。
因此,現代瀏覽器為我們提供了原生的對話框元素:
<dialog> <form method="dialog"> <h3>this is a pretty dialog</h3> <button type="submit">close</button> </form></dialog>
默認情況下,對話框元素不會顯示出來。現在,我會稍微作弊一下,通過 javascript 來控制其顯示和隱藏:
document.queryselector("button").addeventlistener("click", () => { document.queryselector("dialog").showmodal();});
雖然行業正在推動不依賴 javascript 來控制對話框的顯示,有一些工作正在進行中,但它們尚未完全規範化,更不用說實現了。所以目前,我們仍然需要使用 javascript 來打開對話框。但僅此而已,其餘的都是原生的 html 和 css。
對話框元素具有一個叫做 showmodal() 的函數,通過它來打開對話框。對話框會打開在一個稱為 top layer 的東西上面,這是瀏覽器中的一個新概念。如果你想了解更多信息,請參閱 mdn 上關於 top layer 的解釋。
top layer 是一個與你的 html 分離的新層,你可以將元素“提升”到這個層級。這意味着,不管元素的 z-index 和堆疊上下文嵌套如何,位於 top layer 的元素將始終在其他所有元素之上。
你可能會注意到瀏覽器並沒有給打開狀態的對話框提供任何用戶界面。對話框實際上只是一個 div 元素(不是按鈕!),因此你需要自己為對話框提供用於關閉的用戶界面。這就是上面代碼中的表單元素的作用。你可能已經注意到,該表單的 method 屬性值是"dialog"。此時,當該表單被提交時,瀏覽器會將其視為關閉對話框的信號。
通過這種方式,你還可以創建確認對話框,提供兩個按鈕,每個按鈕都有自己的值。
<dialog> <form method="dialog"> <p>tabs or spaces?</p> <button type="submit" value="wrong">tabs</button> <button type="submit" value="correct">spaces</button> </form></dialog>
可以通過監聽對話框的 close 事件來處理用戶的按鈕點擊,並通過“returnvalue”屬性來獲取對應按鈕的值。
dialog.addeventlistener("close", function () { console.log(dialog.returnvalue);});
如果對話框中還有其他表單數據,你也可以以 formdata 的方式讀取它們。
由於對話框本質上是一個 div 元素,你可以根據自己的喜好進行樣式設置。瀏覽器會自動將其居中顯示在屏幕上,但其他所有內容你都可以自定義。
除此之外,對話框還提供了一個稱為::backdrop 的新偽元素。它位於對話框和頁面其他部分之間,你可以對其進行樣式設置,例如調暗頁面的其他部分或以其他方式引導用戶關注對話框。例如,你可以添加一個白色覆蓋層並模糊頁面。
dialog::backdrop { background: #fff5; backdrop-filter: blur(4px);}
與對話框元素本身一樣,背景層也是由瀏覽器自動進行定位。因此你不需要擔心頁面滾動、固定元素和瀏覽器大小調整等情況。這一切都由瀏覽器自動處理。
通過這篇文章,我希望你從中能發現了一些讓你在下一個項目中可以少用一點 javascript 的東西。不過,每當你將一個經過大量實戰驗證的實現方式改成新的方式時,最好進行測試,特別是涉及到可訪問性的時候,以確保你不會忽略任何用戶。
下面這些是我沒有加到本文中的示例:
使用 scroll-behavior: smooth 實現原生平滑滾動(但僅當 prefers-reduced-motion: no preference 匹配時才能生效),
使用 scroll-snap 實現原生輪播圖,
使用 position: sticky 實現“視圖內固定”的元素,
……以及所有的和容器查詢相關的示例。
而且,如果我們展望未來的話,我們將會有更多很酷的東西:
滾動驅動的動畫
使用 grid-template-rows: masonry 替代 masonry.js 實現砌磚布局
使用新的 selectlist 元素實現完全可樣式化的選擇框(你可以對選擇框的每個部分進行樣式設置,而不破壞其所帶來的所有原生功能)
:has() 選擇器將消除一整類 javascript 選擇器
這篇文章是我在一個會議上做的演講的改編版本,如何想更詳細地了解關於本文內容的介紹或其他主題,你可以在觀看視頻:不要再通過 javascript 實現它們:遷移 js 功能到 css 和 html(https://www.youtube.com/watch?v=ztmuju26b7q)
所以,讓我再次強調這篇文章的主要觀點:
僅僅因為你知道某個功能需要 javascript,但並不意味着它仍然需要。如果你不時地測試這些嘗試,你可以製作出更好的網站。
https://www.htmhell.dev/adventcalendar/2023/2/
聲明:本文為 infoq 翻譯整理,未經許可禁止轉載。