2-0

純CSS點擊下拉式選單

純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會跑到某個lia元件上,導致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. 美觀問題

到這裡為止,功能上的問題都處理完畢,剩下幾個美觀問題:

  1. 選單標籤(以及無內容的div元件)收到focus時,周圍會出現一圈外框;
  2. 選單標籤快速點兩下時,文字會被反白選取;
  3. 在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技術分享文看一看。

如果大家有甚麼建議或其它做法,歡迎提出來探討!

4筆討論 回應文章
Arthur Liao5年(更新)

補充一下:另外一種純CSS的做法,是利用<input type="checkbox" id=...><label for=...>,可以參考Norman Chen的CodePen:http://codepen.io/nanron0919/pen/NrdZoE

這種做法所用的CSS更簡單,缺點是沒辦法在點按畫面其它地方的時候,不透過JavaScript而能把選單收起來。(用CSS的:focus選擇器則可以,因為一點畫面其它地方,span:focus就消失了,選單也會跟著收起來。)如果你能接受這個缺點,可以考慮採取input checkbox的做法。

Arthur Liao5年(更新)

有人用佔滿整個畫面的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解決呢?