僅靠H5標籤就能實現收拉效果?我說的是真的!

語言: CN / TW / HK

前言

最近做專案時碰到這麼一個需求:

這有點類似於手風琴效果,但不一樣的是很多手風琴效果是同一時間內只能有一個展開,而這個是各個部分獨立的,你展不展開完全不會影響我的展開與否。其實這種效果簡直再普遍不過了,網上隨便一搜就出來一大堆。但不一樣的是,我在接到這個需求的時候突然想起來很久以前看過張鑫旭大佬的一篇文章,模糊的記得那篇文章裡說過有個什麼很方便的 CSS 屬效能夠實現這一效果,不用像咱們平時實現的那些展開收起那樣寫很多的程式碼,於是就來到他的部落格裡面一頓搜,找了半天終於發現原來是我記錯了,並不是什麼 CSS3 屬性,而是 HTML5 標籤!

details

想要非常輕鬆的實現一個收拉效果,需要用到三個標籤,分別是:<details><summary>以及隨意

隨意是什麼意思?意思是什麼標籤都可以?

咱們先只寫一個<details>標籤來看看頁面上會出現什麼:

<details></details>
複製程式碼

執行結果:

可以看到非常有意思的一個現象:我們明明什麼文字都沒有寫,但頁面上卻出現了詳細資訊這四個字,因為如果你在標籤裡沒有寫<summary>的話,瀏覽器會自動給你補上一個<summary>詳細資訊</summary>,那有人可能奇怪了,怎麼補的是中文呢?那老外不寫<summary>的話也會來一個<summary>詳細資訊</summary>?其實是這樣:

現代瀏覽器經常偷偷獲取使用者隱私資訊,包括但不僅限於用人工智慧判斷螢幕前的使用者是中國人還是外國人,然後根據使用者的母語來動態向<summary>標籤里加入不同語言的'詳細資訊'這幾個字。

開個玩笑,其實是根據你當前作業系統的語言來判斷的,要是你把系統語言改成其它語言的話出現的就不再是'詳細資訊'這幾個中文字元了。

那如果我們在<details>標籤裡寫了<summary>呢?

<details>
  <summary>公眾號:</summary>
</details>
複製程式碼

執行結果:

可以看到<summary>裡面的文字就會在三角箭頭旁邊的標題位置展示出來,可是我們展開三角箭頭髮現裡面什麼內容也沒有,那麼內容寫在哪呢?

只需寫在<summary>的後面就可以了,那是不是還要寫個固定標籤呢?比如什麼<describe>之類的,其實在<summary>之後無論寫什麼標籤都可以,當然必須得是合法的 HTML 標籤啊,比如我們寫個<h1>標籤來試試看:

<details>
  <summary>公眾號:</summary>
  <h1>前端學不動</h1>
</details>
複製程式碼

執行結果:

再換個別的標籤試試:

<details>
  <summary>公眾號:</summary>
  <button>前端學不動</button>
</details>
複製程式碼

執行結果:

看!我們僅用了三個標籤就完成了一個最簡單的收拉效果!以前在網上看到類似的效果要麼就是 getElementById 獲取到 DOM 元素,然後新增 onclick 事件控制下方元素的 style 屬性,要麼就是純 CSS 實現,寫幾個單選按鈕配合兄弟選擇器來控制後方元素的顯隱,抑或是 CSS 與 JS 相結合來實現的,但僅靠 HTML 標籤來實現這一效果還是非常清新脫俗的!並且十分簡潔、非常節約程式碼量、也更加直觀易於理解。

深入測試

既然<summary>標籤後面寫什麼都行,那麼可不可以寫很多個標籤呢?我們來測試一下:

<details>
  <summary>公眾號:</summary>
  <button>前端學不動</button>
  <span>前端學不動</span>
  <h1>前端學不動</h1>
  <a href="#">前端學不動</a>
  <strong>前端學不動</strong>
</details>
複製程式碼

執行結果:

那展開收起那部分的內容只能放在<summary>標籤之後嗎?如果放它前面呢:

<details>
  <button>前端學不動</button>
  <span>前端學不動</span>
  <h1>前端學不動</h1>
  <a href="#">前端學不動</a>
  <strong>前端學不動</strong>
  <summary>公眾號:</summary>
</details>
複製程式碼

執行結果:

效果居然一模一樣,看來展開收起的那部分應該是在<details>標籤內部的除<summary>標籤之外的所有內容。那如果寫兩個<summary>標籤呢:

<details>
  <button>前端學不動</button>
  <span>前端學不動</span>
  <h1>前端學不動</h1>
  <a href="#">前端學不動</a>
  <strong>前端學不動</strong>
  <summary>公眾號:</summary>
  <summary>summary</summary>
</details>
複製程式碼

執行結果:

可以看到只有第一個出現的<summary>標籤是真正的summary,後續出現的其他所有標籤(包括其它的<summary>)都是展開收起的那部分。

既然所有標籤都可以,那麼也包括<details>咯?

<details>
  <summary>project</summary>
  <details>
    <summary>html</summary>
    index.html
  </details>
  <details>
    <summary>css</summary>
    reset.css
  </details>
  <details>
    <summary>js</summary>
    main.js
  </details>
</details>
複製程式碼

執行結果:

這玩意有點意思,利用這種巢狀寫法可以輕鬆實現編輯器左側的那些檔案區的效果。

加入樣式

雖然可以很輕鬆、甚至在不用寫 CSS 程式碼的情況下就實現展開收起效果,但畢竟不寫 CSS 只是實現了個最基礎的乞丐版效果,很多人都不想要點選的時候出現的那個輪廓:

在谷歌瀏覽器和 Safari 瀏覽器下都會出現這個輪廓,火狐就沒有這玩意,咱們只需要給<summary>標籤設定 outline 屬性就可以了,一般如果你的專案引入了抹平瀏覽器樣式間差異的 reset.css 檔案的話,就不用寫這個 CSS 了,為了方便同時觀看 HTML、CSS 和 JS,我們來用 Vue 的格式來寫程式碼:

<template>
  <details>
    <summary>project</summary>
    <details>
      <summary>html</summary>
      index.html
    </details>
    <details>
      <summary>css</summary>
      reset.css
    </details>
    <details>
      <summary>js</summary>
      main.js
    </details>
  </details>
</template>

<style>
summary { outline: none }
</style>
複製程式碼

執行結果:

這樣看起來就舒服多啦!但是還有個問題:那個三角箭頭太傻大黑粗了,一般我們很少會用這樣的箭頭,而且我們也不一定非得讓它在左邊待著,那麼怎麼修改箭頭的樣式呢?

在谷歌瀏覽器以及 Safari 瀏覽器下我們需要用::-webkit-details-marker偽元素,在火狐瀏覽器下我們要用::-moz-list-bullet偽元素,比如我們想讓它別那麼傻大黑粗:

<template>
  <details>
    <summary>project</summary>
    <details>
      <summary>html</summary>
      index.html
    </details>
    <details>
      <summary>css</summary>
      reset.css
    </details>
    <details>
      <summary>js</summary>
      main.js
    </details>
  </details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker {
    transform: scale(.5);
    color: gray
}

/* 火狐 */
::-moz-list-bullet { color: gray }
</style>
複製程式碼

執行結果:

是不是沒那麼傻大黑粗了,不過有時我們不想要這個三角形的箭頭,想要的是自己自定義的箭頭,那麼我們就需要先把這個預設的三角給隱藏掉:

<template>
  <details>
    <summary>project</summary>
    <details>
      <summary>html</summary>
      index.html
    </details>
    <details>
      <summary>css</summary>
      reset.css
    </details>
    <details>
      <summary>js</summary>
      main.js
    </details>
  </details>
</template>

<style>
summary { outline: none }

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }
</style>
複製程式碼

執行結果:

這回箭頭沒了,我們只需要在<summary>標籤裡寫個箭頭就好了,可以用::before::after偽元素,也可以直接在裡面寫個<img>標籤,為了讓大家能夠直接複製程式碼到 Vue 環境裡執行,在這裡我們就不用圖片了,直接手寫<svg>

<template>
  <details>
    <summary>
      <svg width="16" height="7">
        <polyline points="0,0 8,7 16,0"/>
      </svg>
      project
    </summary>
    <details>
      <summary>
        <svg width="16" height="7">
          <polyline points="0,0 8,7 16,0"/>
        </svg>
        html
      </summary>
      index.html
    </details>
    <details>
      <summary>
        <svg width="16" height="7">
          <polyline points="0,0 8,7 16,0"/>
        </svg>
        css
      </summary>
      reset.css
    </details>
    <details>
      <summary>
        <svg width="16" height="7">
          <polyline points="0,0 8,7 16,0"/>
        </svg>
        js
      </summary>
      main.js
    </details>
  </details>
</template>

<style>
summary {
  position: relative;
  padding-left: 20px;
  outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
  position: absolute;
  left: 0;
  top: 50%;
  fill: none;
  stroke: gray
}
</style>
複製程式碼

執行結果:

箭頭是變成自定義的了,但是方向卻不智慧了,不能像原生箭頭那樣展開收起時會自動改變方向,但是<details>這個標籤好就好在它在展開是會自動在標籤裡新增一個open屬性:

我們可以利用它的這一特點,用屬性選擇器來讓<svg>標籤進行旋轉:

<template>
  <details>
    <summary>
      <svg width="16" height="7">
        <polyline points="0,0 8,7 16,0"/>
      </svg>
      project
    </summary>
    <details>
      <summary>
        <svg width="16" height="7">
          <polyline points="0,0 8,7 16,0"/>
        </svg>
        html
      </summary>
      index.html
    </details>
    <details>
      <summary>
        <svg width="16" height="7">
          <polyline points="0,0 8,7 16,0"/>
        </svg>
        css
      </summary>
      reset.css
    </details>
    <details>
      <summary>
        <svg width="16" height="7">
          <polyline points="0,0 8,7 16,0"/>
        </svg>
        js
      </summary>
      main.js
    </details>
  </details>
</template>

<style>
summary {
  position: relative;
  padding-left: 20px;
  outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
  position: absolute;
  left: 0;
  top: 50%;
  transform: rotate(180deg);
  transition: transform .2s;
  fill: none;
  stroke: gray
}

[open] > summary > svg { transform: none }
</style>
複製程式碼

執行結果:

用 JS 控制 open 屬性

既然展開時會自動給<details>標籤新增一個open屬性,那如果我們用 JS 手動給<details>標籤新增或刪除open屬性,<details>標籤會隨之展開收起嗎?

比如我們用定時器,每隔1秒就自動展開一個,同時收起上一個已被展開過的標籤:

<template>
  <details v-for="({title, content}, index) of list" :key="title" :open="openIndex === index">
    <summary>
      <svg width="16" height="7">
        <polyline points="0,0 8,7 16,0"/>
      </svg>
      {{ title }}
    </summary>
    {{ content }}
  </details>
</template>

<script>
import { defineComponent, ref, onBeforeUnmount } from 'vue'

export default defineComponent(() => {
  const list = [{
    title: 'html',
    content: 'index.html'
  }, {
    title: 'css',
    content: 'reset.css'
  }, {
    title: 'js',
    content: 'main.js'
  }]

  const openIndex = ref(-1)

  const interval = setInterval(() => openIndex.value === list.length
    ? openIndex.value = 0
    : openIndex.value++
  , 1000)

  onBeforeUnmount(() => clearInterval(interval))

  return { list, openIndex }
})
</script>

<style>
summary {
  position: relative;
  padding-left: 20px;
  outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
  position: absolute;
  left: 0;
  top: 50%;
  transform: rotate(180deg);
  transition: transform .2s;
  fill: none;
  stroke: gray
}

[open] > summary > svg { transform: none }
</style>
複製程式碼

執行結果:

既然能靠控制open屬性來控制元素的展開收起,那麼手風琴效果也很好實現了:只需要保證在當前列表中僅有一個<details>標籤有open屬性,點選別的標籤時就去掉另一個標籤的open屬性即可:

<template>
  <details
    v-for="({title, content}, index) of list"
    :key="title"
    :open="openIndex === index"
    @toggle="onChange($event, index)"
  >
    <summary>
      <svg width="16" height="7">
        <polyline points="0,0 8,7 16,0"/>
      </svg>
      {{ title }}
    </summary>
    {{ content }}
  </details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
  const list = [{
    title: 'html',
    content: 'index.html'
  }, {
    title: 'css',
    content: 'reset.css'
  }, {
    title: 'js',
    content: 'main.js'
  }]

  const openIndex = ref(-1)

  const onChange = ({ target }, i) => target.open && (openIndex.value = i)

  return { list, openIndex, onChange }
})
</script>

<style>
summary {
  position: relative;
  padding-left: 20px;
  outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
  position: absolute;
  left: 0;
  top: 50%;
  transform: rotate(180deg);
  transition: transform .2s;
  fill: none;
  stroke: gray
}

[open] > summary > svg { transform: none }
</style>
複製程式碼

執行結果:

⚠️需要注意的是,在<details>標籤展開收起時會觸發一個 toggle 事件,和 click、mousemove 等事件用法一致,也會接收一個 event 物件的引數,event.target 是當前觸發事件的 DOM,也就是<details>,它會有一個.open屬性,值為 true 或 false,代表是否展開收起。

加入動畫

那麼接下來離一個理想的手風琴效果只差最後一步了:過渡動畫

但過渡動畫這裡有坑,我們先來分析一下思路:在平時就給<details>標籤裡的內容區(除第一個出現的

標籤以外的內容)寫上:max-height: 0;
然後在 open 時用屬性選擇器 [open] 配合後代選擇器來給內容區加上 max-height: xxx; 的程式碼,這樣平時在收起時高度就是0,等出現 open 屬性時就會慢慢過渡到我們定義的最大高度:

<template>
  <details
    v-for="({title, content}, index) of list"
    :key="title"
    :open="openIndex === index"
    @toggle="onChange($event, index)"
  >
    <summary>
      <svg width="16" height="7">
        <polyline points="0,0 8,7 16,0"/>
      </svg>
      {{ title }}
    </summary>
    <ul>
      <li v-for="doc of content" :key="doc">{{ doc }}</li>
    </ul>
  </details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
  const list = [{
    title: 'html',
    content: ['index.html', 'banner.html', 'login.html', '404.html']
  }, {
    title: 'css',
    content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
  }, {
    title: 'js',
    content: ['index.js', 'main.js', 'javascript.js']
  }]

  const openIndex = ref(-1)

  const onChange = ({ target }, i) => target.open && (openIndex.value = i)

  return { list, openIndex, onChange }
})
</script>

<style>
summary {
  position: relative;
  padding-left: 20px;
  outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
  position: absolute;
  left: 0;
  top: 50%;
  transform: rotate(180deg);
  transition: transform .2s;
  fill: none;
  stroke: gray
}

details > ul {
  max-height: 0;
  margin: 0;
  overflow: hidden;
}

[open] > summary > svg { transform: none }
[open] > ul { max-height: 120px }
</style>
複製程式碼

執行結果:

如果用谷歌瀏覽器開啟的話居然看不到任何的過渡效果!但用火狐開啟就有效果:

估計是瀏覽器的 bug,既然過渡動畫(transition)在不同瀏覽器之間表現不一致,那關鍵幀動畫(keyframes)呢?

<template>
  <details
    v-for="({title, content}, index) of list"
    :key="title"
    :open="openIndex === index"
    @toggle="onChange($event, index)"
  >
    <summary>
      <svg width="16" height="7">
        <polyline points="0,0 8,7 16,0"/>
      </svg>
      {{ title }}
    </summary>
    <ul>
      <li v-for="doc of content" :key="doc">{{ doc }}</li>
    </ul>
  </details>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
  const list = [{
    title: 'html',
    content: ['index.html', 'banner.html', 'login.html', '404.html']
  }, {
    title: 'css',
    content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
  }, {
    title: 'js',
    content: ['index.js', 'main.js', 'javascript.js']
  }]

  const openIndex = ref(-1)

  const onChange = ({ target }, i) => target.open && (openIndex.value = i)

  return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
  position: relative;
  padding-left: 20px;
  outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
  position: absolute;
  left: 0;
  top: 50%;
  transform: rotate(180deg);
  transition: transform .2s;
  fill: none;
  stroke: gray
}

details > ul {
  max-height: 0;
  margin: 0;
  overflow: hidden;
}

[open] {
  > summary > svg { transform: none }
  > ul { animation: open .2s both }
}

@keyframes open {
  to { max-height: 120px }
}
</style>
複製程式碼

執行結果:

可以看到關鍵幀動畫在各大瀏覽器的行為都是一致的,推薦大家使用關鍵幀動畫。

收起動畫

上面那種效果已經完全足夠滿足我們的日常開發需求了,但它仍然有一個小小的遺憾,那就是:收起的時候沒有任何的動畫效果。

這是因為<details>的行為是靠著 open 屬性控制內容顯示或隱藏,你可以簡單的把它的隱藏理解為display: block;display: none;,雖然這麼說可能並不準確,但卻非常有助於我們理解<details>的行為:在展開時display: block;突然顯示,既然顯示了就可以有時間展示我們的展開動畫。但在收起時display: none;是突然消失,根本沒時間展示我們的收起動畫。

那麼怎麼才能解決這個問題呢?答案就是更改 DOM 結構,我們把原本放在<details>裡面那部分需要展開收起的內容元素移到<details>標籤的外面去,但一定要在它的後一位,這樣就可以方便我們用兄弟選擇器配合屬性選擇器來控制外部元素的顯隱了,在<details>標籤有 open 屬性時我們就讓它的後面一個元素用動畫展開,沒有 open 屬性時我們就讓後一個元素用動畫收起:

<template>
  <template v-for="({title, content}, index) of list" :key="title">
    <details
      :open="openIndex === index"
      @toggle="onChange($event, index)"
    >
      <summary>
        <svg width="16" height="7">
          <polyline points="0,0 8,7 16,0"/>
        </svg>
        {{ title }}
      </summary>
    </details>
    <ul>
      <li v-for="doc of content" :key="doc">{{ doc }}</li>
    </ul>
  </template>
</template>

<script>
import { defineComponent, ref } from 'vue'

export default defineComponent(() => {
  const list = [{
    title: 'html',
    content: ['index.html', 'banner.html', 'login.html', '404.html']
  }, {
    title: 'css',
    content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
  }, {
    title: 'js',
    content: ['index.js', 'main.js', 'javascript.js']
  }]

  const openIndex = ref(-1)

  const onChange = ({ target }, i) => target.open && (openIndex.value = i)

  return { list, openIndex, onChange }
})
</script>

<style lang="scss">
summary {
  position: relative;
  padding-left: 20px;
  outline: none
}

/* 谷歌、Safari */
::-webkit-details-marker { display: none }

/* 火狐 */
::-moz-list-bullet { font-size: 0 }

svg {
  position: absolute;
  left: 0;
  top: 50%;
  transform: rotate(180deg);
  transition: transform .2s;
  fill: none;
  stroke: gray
}

ul {
  max-height: 0;
  margin: 0;
  transition: max-height .2s;
  overflow: hidden
}

[open] {
  > summary > svg { transform: none }
  + ul { max-height: 120px }
}
</style>
複製程式碼

執行結果:

結語

如果你的專案不需要這些花裡胡哨的動畫效果,完全可以只靠 H5 標籤去實現,根本不必再去關心展開收起的邏輯了,只需要寫一些樣式程式碼就可以了,比如寫成暗黑模式:

你的 CSS 只需要專注於暗黑模式本身就夠了,是不是很省心呢?

同時這個收拉效果也並不僅僅只適用於手風琴,很多地方都可以用到它,比如這種:

但唯一比較遺憾的事就是這個標籤不支援 IE:

不過好在別的瀏覽器支援的都不錯,如果你的專案不需要相容 IE 的話就請盡情的享受<details>標籤所帶來的便利吧!

本文首發於公眾號:《前端學不動》

往期精彩文章