純CSS可以很容易做到hover下拉式選單,但要做到clickable下拉式選單,通常都必須配合JavaScript。在此跟大家分享我自己研究出來的純CSS點擊下拉式選單,範例放在CodePen:http://codepen.io/arthurtw/pen/KMarNG
我的做法,是參考一篇國外文章「A pure CSS onclick menu」,但那篇文章有個最大問題:不支援iOS。不支援IE就算了,不支援iOS等於不能用,所以這個問題必須解決。後面我會說明如何解決iOS的問題。
以下我們就一起來看看,如何用純CSS做到點擊下拉式選單(onclick dropdown menu)。
從hover到clickable
用CSS偵測選單是否被hover很簡單,用:hover
選擇器(selector)即可。那麼如何用CSS偵測選單是否被點擊呢?這件事本身不難,我們可以用:focus
選擇器。假設我們的選單HTML如下:
<div class="dropdown-menu">
<span tabindex="0">Click Me!</span>
<ul>
<li><a href="#" onclick="alert('click 1')">Item 1</a></li>
<li><a href="#" onclick="alert('click 2')">Item 2</a></li>
<li><a href="#" onclick="alert('click 3')">Last Item</a></li>
</ul>
</div>
其中ul
平常是隱藏狀態,只有span
元件(也就是選單標籤「Click Me!」)被點擊時才會顯示,此時span
元件會收到focus。(注意到以上span
元件有個tabindex="0"
屬性嗎?這是為了讓它可以收到focus。)我們的CSS可以這樣設計:
.dropdown-menu {
position: relative;
}
.dropdown-menu > ul {
position: absolute;
z-index: 1;
display: none;
}
.dropdown-menu > span:focus ~ ul {
display: block;
}
以上我們看到,ul
元件平常是display: none;
,但如果它前面有個span:focus
元件,就會變成display: block;
,這就是clickable dropdown menu最基本的原理。
接下來,我們要逐一解決碰到的問題。
1. 選項無法點擊
以上的選單根本無法使用,因為點了「Click Me!」,選單雖然打開了,但點擊任一選項,onclick
事件處理器卻沒有被觸發。這是因為選項一點下去(例如點了「Item 1」),focus會跑到某個li
的a
元件上,導致span
元件(也就是「Click Me!」)的focus瞬間消失。因為span:focus
沒了,整個ul
也跟著消失。(.dropdown-menu > span:focus ~ ul
規則不再起作用,所以ul
元件回到display: none;
狀態。)
解決辦法至少有兩個。一是國外那篇文章中的解法,用visibility
屬性取代display
屬性,配合0.5秒鐘的transition
,讓瀏覽器有時間去觸發選項的onclick
事件處理器;缺點是不支援IE9(transition
屬性要到IE10才出現)。二是增加ul:hover
規則,讓ul
元件在span:focus
消失的情況下還能繼續顯示;缺點是點了選項之後,選單不會立刻消失,要等游標離開選單區域,所以user experience差一些。
在這篇文章中,我們將採用第一種解法:放棄對IE9的支援。如果你必須支援IE9,可以將以上的.dropdown-menu > span:focus ~ ul { ... }
規則改成.dropdown-menu > span:focus ~ ul, .dropdown-menu > ul:hover { ... }
(也就是增加.dropdown-menu > ul:hover
規則)。
採用第一種解法,修改後的CSS如下:
.dropdown-menu > ul {
position: absolute;
z-index: 1;
visibility: hidden;
transition: visibility 0.5s;
}
.dropdown-menu > span:focus ~ ul {
visibility: visible;
}
2. 選項會短暫停留0.5秒
採用第一種解法之後,有個後遺症:選項在消失之前,會在畫面短暫停留0.5秒,感覺頓頓的。我們可以用opacity: 0;
屬性,讓選項看似瞬間消失(因為元件變透明了,實際上元件還會存在0.5秒):
.dropdown-menu > ul {
position: absolute;
z-index: 1;
visibility: hidden;
transition: visibility 0.5s;
opacity: 0;
}
.dropdown-menu > span:focus ~ ul {
visibility: visible;
opacity: 1;
}
3. 不支援iOS
前面提到,國外那篇文章的做法,並不支援iOS。這是因為iOS Safari瀏覽器不把span
看作是可以點擊的元件。我研究出來的解決辦法,是為span
元件(也就是「Click Me!」文字)加上onclick="return true"
屬性,讓iOS Safari將此元件視為clickable。修改後的HTML如下:
<span tabindex="0" onclick="return true">Click Me!</span>
不過iOS支援的問題還沒結束,下面的問題也跟iOS有關。
4. 再次點擊選單,不能將選項收起來
對使用者而言,點一次選單標籤(也就是「Click Me!」文字)可以展開選項,再點一次選單標籤,應該要將選項收起來。目前為止我們的做法,達不到這個效果,因為點了一次選單標籤,focus就在選單標籤上了,再點一次選單標籤,focus還是在同一個地方。
國外那篇文章的做法,是利用CSS屬性pointer-events
,當span
元件一收到focus,就將它的pointer-events
屬性設成none
,並將外面一層元件(也就是<div class="dropdown-menu">
)的pointer-events
屬性設成auto
。但這種做法在iOS Safari會有問題,因為元件光有pointer-events: auto
屬性不夠,必須有onclick
事件處理器,iOS Safari才會將此元件視為clickable。而我們沒辦法透過CSS去動態改變HTML元件的onclick
事件處理器。
我採用的解決辦法,是疊一個無內容的div
上去。平時這個div
元件是隱藏的(display: none
),當span
元件一收到focus,就顯示這個div
元件(display: block
);因為它疊在span
元件上面,所以再次點擊選單標籤「Click Me!」時,點到的其實是這個div
元件,這樣focus跑到div
元件上,span:focus
沒了,選項就隱藏起來了,此時div
元件也跟著回到隱藏狀態。
修改後的HTML如下:(注意div
元件一樣要有tabindex="0"
及onclick="return true"
屬性)
<div class="dropdown-menu">
<span tabindex="0" onclick="return true">Click Me!</span>
<div tabindex="0" onclick="return true"></div>
<ul>
<li><a href="#" onclick="sampleMenu(this)">Item 1</a></li>
<li><a href="#" onclick="sampleMenu(this)">Item 2</a></li>
<li><a href="#" onclick="sampleMenu(this)">Last Item</a></li>
</ul>
</div>
CSS修改如下:(其中div
元件的top: 0; left: 0; right: 0; bottom: 0;
屬性,是為了讓它佔滿父元件的整個空間)
.dropdown-menu > div {
background-color: rgba(0, 0, 0, 0);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: none;
}
.dropdown-menu > span:focus ~ div {
display: block;
}
5. 美觀問題
到這裡為止,功能上的問題都處理完畢,剩下幾個美觀問題:
- 選單標籤(以及無內容的
div
元件)收到focus時,周圍會出現一圈外框; - 選單標籤快速點兩下時,文字會被反白選取;
- 在iOS Safari上,點擊選單標籤,會出現一個灰底方框一閃而過。
第1個問題,可以將CSS屬性outline
設定為0;第2個問題,可以將CSS屬性user-select
設定為none
;第3個問題,可以將CSS屬性-webkit-tap-highlight-color
設定成透明顏色。修改後的CSS如下:
.dropdown-menu > span {
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.dropdown-menu > span,
.dropdown-menu > div {
cursor: pointer;
outline: 0;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
6. 與FastClick的相容性
如果你加掛了FastClick程式庫(一個用來消除行動裝置瀏覽器點擊延遲的工具),必須為選單標籤加上CSS類別needsclick
:
<span tabindex="0" onclick="return true" class="needsclick">Click Me!</span>
你現在正在瀏覽的Twincl網站,網頁右上角功能選單(登入後才會看到)就是用本文介紹的純CSS做出來的。這個網站也有加掛FastClick。不過我為了支援IE9,第1個問題是用解法2(ul:hover
)。
7. 行動裝置上的選單收合問題
在行動裝置上,還有一個問題:打開選單後,點畫面其它地方,選單不會自動收起來。這是因為畫面的空白區域不能「接收」focus,電腦上則沒有這個問題。我有找到一個解決辦法,就是在網頁最外層包一個可以接收focus的div
:
<div style="-webkit-tap-highlight-color:rgba(0,0,0,0)" onclick="return true">
...
</div>
理論上這個div
只會影響行動裝置,但畢竟多了一層div
,所以我對這個workaround不是很滿意。如果你能接受行動裝置上的這個小問題,就可以不用多加這一層div
。
結論
以上講了一大堆,目標只有一個:用純CSS做出點擊下拉式選單,並在絕大部份瀏覽器及行動裝置上正常使用。如果你沒有這個需求,繼續用你習慣的JavaScript程式庫就行啦,這篇文章就當作CSS技術分享文看一看。
如果大家有甚麼建議或其它做法,歡迎提出來探討!
補充一下:另外一種純CSS的做法,是利用
<input type="checkbox" id=...>
及<label for=...>
,可以參考Norman Chen的CodePen:http://codepen.io/nanron0919/pen/NrdZoE這種做法所用的CSS更簡單,缺點是沒辦法在點按畫面其它地方的時候,不透過JavaScript而能把選單收起來。(用CSS的
:focus
選擇器則可以,因為一點畫面其它地方,span:focus
就消失了,選單也會跟著收起來。)如果你能接受這個缺點,可以考慮採取input checkbox的做法。有人用佔滿整個畫面的overlay作為label,解決選單收不起來的問題:http://www.felipefialho.com/css-components/#component-dropdown
但這種做法會導致一個新的usability issue,就是那層overlay會吃掉畫面原本的mouse click event。我做了另一個CodePen示範這個問題:http://codepen.io/arthurtw/pen/Vjpmwq
在dropdown關起來的時候,CodePen範例下方的link可以作用,但當dropdown打開的時候,那個link是點不到的,因為實際上點到的是overlay。
這個方法行動裝置上測試,touch其他地方的時候選單無法收合,請問是不是只能透過javascript解決呢?
我有找到一個workaround,請見文章的第7點。CodePen也更新了:http://codepen.io/arthurtw/pen/KMarNG