🪗

details・summaryタグで開閉アニメーション付きアコーディオンUIを作ろう

2024/01/04に公開

FAQなんかのUIでよく使われる、クリックしたら関連するコンテンツがニュッと表示されるアレ。
みなさんは普段どのように実装されていますか?

アコーディオンUIを実装するにあたって、マークアップに最適なタグとしてdetailssummaryがあると知りまして、このタグを使って実装してみたいと思います。

実装してみた

まずは結論から。
実際に今回実装していくアコーディオンUIのソースコードはこちらです。

details・summaryタグは何が良いの?

details summaryタグを使用しない実装では、アクセシビリティな実装をする場合、考慮する事項が多く、網羅しようと思うと工数がかかります。

ですが、details summary要素を使用することでアクセシビティを考慮したUIが簡単に作成できます。

メリット

マークアップが簡潔

divタグ inputタグ等を使って実装するよりもHTMLが簡潔でわかりやすくなります。
また、ブラウザがよしなに開閉動作も含めて表示してくれるので、CSS・JSの実装工数も削減可能に。

index.html
<details class="details">
  <summary class="summary">質問</summary>
 回答
</details>

簡単にアクセシビリティ対策

以下の機能をdetails summaryを使うだけで実装できます

  • TABフォーカスが有効になる。
  • かつフォーカスが当たっている状態でspaceキーorEnterキーで開閉ができる
  • 隠れているコンテンツもページ内検索の対象になる
  • スクリーンリーダーが開閉状態を適切に読み上げてくれる

デメリット

特に思い当たりませんでした。。

強いて言うなら、クリック時の開閉の挙動がデフォルトのままだとdisplay:none;の切替のような動作となります。
高さがヌルッと変わって表示されるようなUIなど、アニメーションを含めた開閉表示をしたい場合には少し工夫が必要となります。
これについては後ほど説明します。

実装

それでは、実装しつつ解説していきます。

HTML

ベースとなるHTMLはこちら。
上記で出したマークアップとほとんど変わりません。
回答となる部分のみ階層化しています。

index.html
<details class="details">
  <summary class="summary">質問</summary>
  <div class="answer">
    <div class="answerInner">回答</div>
  </div>
</details>

CSS

CSSもかなりシンプルです。

style.css
.summary {
  display: block; /*デフォルトの三角形を削除*/
  cursor: pointer;
  padding: 20px;
}
.summary::-webkit-details-marker {
  /* Safari-デフォルトの三角形を削除*/
  display: none;
}
.answer {
  overflow: hidden;
  /* padding・marginはここでは設定しない */	
}
.answerInner {
  padding: 0 20px 20px;
}

ポイントとしては、summaryタグはデフォルトで開閉を現す三角形が表示されます。デザインに合わせてアイコンなどを使用するケースがほとんどだと思いますので、デフォルトの三角形は削除してしまいます。

style.css
.summary {
  display: block;
}

Safariのみ-webkit-details-markerという擬似要素で三角形が表示されているようなので、display:noneを指定して削除します。

style.css
.summary::-webkit-details-marker {
  /* Safari-デフォルトの三角形を削除*/
  display: none;
}

HTML・CSS

ここまでのソースコードで作成したUIはこちら。
JSを使用せずとも開閉動作を含めたUIが簡単に作成できました。

Javascript

ここまででほぼ完成ですが、開閉時に高さがスムーズに可変するアニメーションを実装していきます。
Web Animation API
を使用してJSからheight opacityを操作してアニメーションを実装します。

イージングとopen時、close時のアニメーションは予め関数としてまとめておきます。

script.js
// アニメーションの時間とイージング
const animTiming = {
  duration: 300,
  easing: "ease-in-out",
};

// アコーディオンを閉じるときのキーフレーム
const closingAnimation = (answer) => [
  {
    height: answer.offsetHeight + "px",
    opacity: 1,
  },
  {
    height: 0,
    opacity: 0,
  },
];

// アコーディオンを開くときのキーフレーム
const openingAnimation = (answer) => [
  {
    height: 0,
    opacity: 0,
 },
 {
    height: answer.offsetHeight + "px",
    opacity: 1,
  },
];

ソースコードはこちら

script.js
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll(".details").forEach(function (el) {
		const summary = el.querySelector(".summary");
		const answer = el.querySelector(".answer");
		summary.addEventListener("click", (event) => {
			// デフォルトの挙動を無効化
			event.preventDefault();
			// detailsのopen属性を判定
			if (el.getAttribute("open") !== null) {
				// アコーディオンを閉じるときの処理
				const closingAnim = answer.animate(closingAnimation(answer), animTiming);

				closingAnim.onfinish = () => {
					// アニメーションの完了後にopen属性を取り除く
					el.removeAttribute("open");
				};
			} else {
				// open属性を付与
				el.setAttribute("open", "true");
				// アコーディオンを開くときの処理
				const openingAnim = answer.animate(openingAnimation(answer), animTiming);
			}
		});
	});
});

ポイント:デフォルト挙動の無効化

script.js
summary.addEventListener("click", (event) => {
	// デフォルトの挙動を無効化
	event.preventDefault();
	~~~~~~~以下省略~~~~~~~

summaryクリック時の挙動はdisplay:none;のような挙動に近く、transitionなどでアニメーションを設定しても即開閉となってしまいます。

その為、デフォルトのsummaryの挙動をpreventDefaultで無効化します。

無効化すると、open属性の着脱の挙動がなくなってしまうので、JSで着脱されるようにします。

script.js
summary.addEventListener("click", (event) => {
	// デフォルトの挙動を無効化
	event.preventDefault();
	// detailsのopen属性を判定
	if (el.getAttribute("open") !== null) {
		// アコーディオンを閉じるときの処理
		const closingAnim = answer.animate(closingAnimation(answer), animTiming);

		closingAnim.onfinish = () => {
			// アニメーションの完了後にopen属性を取り除く
			el.removeAttribute("open");
		};
	} else {
		// open属性を付与
		el.setAttribute("open", "true");
		// アコーディオンを開くときの処理
		const openingAnim = answer.animate(openingAnimation(answer), animTiming);
	}
});

完成

まとめ

以上、detailssummaryを使用したアコーディオンUIの実装でした。
使い回しが効く様にシンプルな実装にしています。
開閉を表す三角形など細かなUIは実装していませんので、自由にカスタマイズしてみてください。

参考

<details>: 詳細折りたたみ要素
<summary>: 概要明示要素

Discussion