details・summaryタグで開閉アニメーション付きアコーディオンUIを作ろう
FAQなんかのUIでよく使われる、クリックしたら関連するコンテンツがニュッと表示されるアレ。
みなさんは普段どのように実装されていますか?
アコーディオンUIを実装するにあたって、マークアップに最適なタグとしてdetails
とsummary
があると知りまして、このタグを使って実装してみたいと思います。
実装してみた
まずは結論から。
実際に今回実装していくアコーディオンUIのソースコードはこちらです。
details・summaryタグは何が良いの?
details
summary
タグを使用しない実装では、アクセシビリティな実装をする場合、考慮する事項が多く、網羅しようと思うと工数がかかります。
ですが、details
summary
要素を使用することでアクセシビティを考慮したUIが簡単に作成できます。
メリット
マークアップが簡潔
divタグ
inputタグ
等を使って実装するよりもHTMLが簡潔でわかりやすくなります。
また、ブラウザがよしなに開閉動作も含めて表示してくれるので、CSS・JSの実装工数も削減可能に。
<details class="details">
<summary class="summary">質問</summary>
回答
</details>
簡単にアクセシビリティ対策
以下の機能をdetails
summary
を使うだけで実装できます
- TABフォーカスが有効になる。
- かつフォーカスが当たっている状態で
spaceキー
orEnterキー
で開閉ができる - 隠れているコンテンツもページ内検索の対象になる
- スクリーンリーダーが開閉状態を適切に読み上げてくれる
デメリット
特に思い当たりませんでした。。
強いて言うなら、クリック時の開閉の挙動がデフォルトのままだとdisplay:none;
の切替のような動作となります。
高さがヌルッと変わって表示されるようなUIなど、アニメーションを含めた開閉表示をしたい場合には少し工夫が必要となります。
これについては後ほど説明します。
実装
それでは、実装しつつ解説していきます。
HTML
ベースとなるHTMLはこちら。
上記で出したマークアップとほとんど変わりません。
回答
となる部分のみ階層化しています。
<details class="details">
<summary class="summary">質問</summary>
<div class="answer">
<div class="answerInner">回答</div>
</div>
</details>
CSS
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タグ
はデフォルトで開閉を現す三角形が表示されます。デザインに合わせてアイコンなどを使用するケースがほとんどだと思いますので、デフォルトの三角形は削除してしまいます。
.summary {
display: block;
}
Safariのみ-webkit-details-marker
という擬似要素で三角形が表示されているようなので、display:none
を指定して削除します。
.summary::-webkit-details-marker {
/* Safari-デフォルトの三角形を削除*/
display: none;
}
HTML・CSS
ここまでのソースコードで作成したUIはこちら。
JSを使用せずとも開閉動作を含めたUIが簡単に作成できました。
Javascript
ここまででほぼ完成ですが、開閉時に高さがスムーズに可変するアニメーションを実装していきます。
Web Animation API
を使用してJSからheight
opacity
を操作してアニメーションを実装します。
イージングとopen時、close時のアニメーションは予め関数としてまとめておきます。
// アニメーションの時間とイージング
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,
},
];
ソースコードはこちら
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);
}
});
});
});
ポイント:デフォルト挙動の無効化
summary.addEventListener("click", (event) => {
// デフォルトの挙動を無効化
event.preventDefault();
~~~~~~~以下省略~~~~~~~
summary
クリック時の挙動はdisplay:none;
のような挙動に近く、transitionなどでアニメーションを設定しても即開閉となってしまいます。
その為、デフォルトのsummaryの挙動をpreventDefault
で無効化します。
無効化すると、open属性の着脱の挙動がなくなってしまうので、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);
}
});
完成
まとめ
以上、details
とsummary
を使用したアコーディオンUIの実装でした。
使い回しが効く様にシンプルな実装にしています。
開閉を表す三角形など細かなUIは実装していませんので、自由にカスタマイズしてみてください。
Discussion