details 要素で作るアコーディオン UI のサンプル

よくありそうな構成のアコーディオン (開閉式) UI を details 要素で実装したサンプルを GitHub に上げたので共有しつつ、details 要素使用時の注意点やスタイリングに関するまとめ。

開閉式のウィジェット、所謂アコーディオン UI を実装する手段として、details 要素を使用する方法は面倒な JavaScript を記述する必要もなく、(正しい構文で HTML を記述すれば) アクセシビリティ的な問題も気にせず簡単に実装することが可能なためよく利用しますが、よくありそうな構成のアコーディオン UI を details 要素で実装したサンプルを GitHub に上げたので共有します。

デモページは GitHub Pages で公開しているので下記から確認できます。

details 要素使用時の注意点

details 要素でアコーディオン UI を実装する際、details 要素に加えて、summary 要素を組み合わせて実装することになります。それぞれの要素の仕様は下記。

まず、details 要素のコンテンツモデルは、「フロー・コンテンツ (Flow content)」 ですが、最初の子要素として summary 要素が 1 つ必須になります。

よって、下記の記述は妥当ですが、

<!-- 🙆 Valid HTML -->
<details>
  <summary>募集要項</summary>
  <p>
     ここにコンテンツ
  </p>
</details>

次のような記述は details 要素の最初の子要素として summary 要素がないので構文エラーです。

<!-- 🙅 Invalid HTML -->
<details>
  <div>
    <summary>募集要項</summary>
  </div>
  <p>
     ここにコンテンツ
  </p>
</details>

仕様を理解していないと、こういう div 要素の使い方はデザイン再現のためにやりがちな気がしますので気をつけましょう。

details 要素で使用可能な属性

details 要素では下記の属性が使用できます。

name 属性

details 要素に付与された name 属性は、details 要素をグループ化します。

複数の details 要素に name 属性で同じ名前を付けると、それらはグループと見なされ、相互に排他的な動作、つまり「ひとつを開いた時に他を閉じる」という動作を実現できます。

この記事執筆時点で Firefox 以外の主要ブラウザではサポートされています。

name 属性を使用する場合、同じ名前を持った details 要素が入れ子関係になったりしないように注意してください。

open 属性

open 属性が指定されると、ページ読み込み時に初期状態で details 要素の内容を表示します。

open 属性は論理属性ですので、<details open><details open="">、あるいは <details open="open"> のような記述が可能です。

summary 要素のコンテンツモデル

summary 要素のコンテンツモデルは、

Phrasing content, optionally intermixed with heading content.

日本語に翻訳すれば、『フレージング・コンテンツ、オプションでヘッディング・コンテンツと混在可能』 です。

コンテンツモデル上は、フレージング・コンテンツである a 要素などを内包できることになりますが、これは避けるべきでしょう。

なぜか、という説明はちょっと回りくどくなるのですが、まず summary 要素は、セマンティクス的には文字通り details 要素の 「要約(summary)」 という意味になるわけですけども、多くのブラウザ (ユーザエージェント) において、summary 要素には、暗黙の ARIA セマンティクスとして role="button" が与えられます (下記参照)。

role="button" は、その子孫要素を role="presentation" を持つものとして扱います。つまり、厳密にいうと、summary 要素内の a 要素は、「リンクである」 という意味を持たなくなりますし、仕様上はフォーカスもできなくなります。

実際にはブラウザがうまいこと処理してしまうので、summary 要素内の a 要素に該当する部分はリンクとして機能して、summary 要素部分を操作すると details 要素が展開する、みたいな動作をすると思いますし、両要素ともキーボードフォーカスもきちんと受け取ると思うんですが、クリックする場所によってリンクになったり、コンテンツの開閉になったりというわかりにくい UI になりますので、原則として summary 要素内に a 要素 (button 要素とかもそうですが) を入れ子にしないようにしましょう。

「メニューとかだと、こういう一部はリンク、一部は開閉ボタンみたいな UI はよくあるじゃん」 と思う方もいらっしゃるかもしれませんが、details 要素は、「開閉式ウィジェット」 を表すもので、リンクリストで構成されるような、所謂 「メニューウィジェット」 の実装に関しては用途違いですので、その辺は気をつけましょう (といいつつ、サンプルには 「それ、メニューじゃん」 って言われそうなデザインも入っていますが)。

details 要素のスタイリングなど

details 要素内で summary 要素が使用された場合は summary 要素内のテキストに、summary 要素が存在しない場合はブラウザが自動的にサマリーとなるテキスト (多くの場合 「詳細」 など) を表示した上で、矢印などのマーカーが表示されます。

このマーカーが不要だな、という場合は、CSS で非表示にすることができます。

details > summary {
  list-style: none;
}

/* WebKit 向けに下記の記述が必要 */
details > summary::-webkit-details-marker {
  display: none;
}

/* わかりやすいように下記の指定もしておくといいかも */
details > summary:hover {
  cursor: pointer;
}

summary 要素に対しては、list-style プロパティ (list-style-image / list-style-position / list-style-type) が使用できますので、マーカーをカスタマイズすることもできます。

また、details 要素が開いた状態は下記のような CSS セレクタでマッチできます。

details[open]  > summary {
  background-color: yellow;
}

今回のサンプルでは開閉状態に応じてアイコンを切り替えたり、回転させたりしていますが、上記のセレクタで実現可能です。

その他、details 要素の状態の変化は JavaScript でも取得可能です。

const detailsList = document.querySelectorAll('details');
detailsList.forEach((details) => {
  details.addEventListener('toggle', (event) => {
    if (details.open) {
      // details 要素が開いている
    } else {
      // details 要素が閉じている
    }
  });
});

ということで、詳細部分を折りたたんで表示したいコンテンツについては、details 要素で作ると、とても簡単ですのでお勧めです。


ちなみに、今回のサンプルをまとめている時に、name 属性を使ったら Astro の型チェックで怒られたために急遽書いたのが下記の関連記事です。

関連エントリー

記事をここまで御覧頂きありがとうございます。
この記事が気に入ったらサポートしてみませんか?