アコーディオン型ユーザーインターフェイス(UI)はウェブページでよくみられる表現です。巷ではさまざまな方法でアコーディオンUIを作る方法が紹介されていますが、みなさんはどのような方法で実装していますか? 見た目だけでなくアクセシビリティ対策までしっかりとできているでしょうか?
<details>
要素と<summary>
要素は、アコーディオンUIを実装するのに最適です。過去にIE対策として<button>
要素や<div>
要素、<input>
要素などでアコーディオンUIを作っていた方は、アクセシビリティ対策が簡単にできるので、<details>
要素と<summary>
要素の採用がオススメです。
この記事では、<details>
要素と<summary>
要素がアコーディオンUIに最適と言える理由と、HTMLのマークアップからCSSでのスタイリング、JavaScriptでのアニメーション制御まで順を追ってアコーディオンUIの作り方をご紹介します。
アコーディオンUIを作る際によくある課題
<details>
要素と<summary>
要素はHTML 5.1(リンク先は2016年当時の仕様書)で登場した比較的新しいタグです。IEサポート終了前の対策としてなのか、<button>
要素や<div>
要素、<input>
要素のtype="checkbox"
などに開閉の動作を追加した作例がアコーディオンUIの作り方として多く見られます。これらはアコーディオンのような開閉動作は作れるものの、以下のようにさまざまな面で課題があります。
コード面
HTMLの構造が複雑。本来の使用用途からずれてしまっているタグで作ると、ひと目見ただけでは何を表すのかが分かりにくいでしょう。
▼<input>
要素で作った例
<input id="accordion" type="checkbox" class="open">
<label class="summary" for="accordion">概要</label>
<div class="content">
コンテンツ詳細
</div>
実装工数の面
必要最低限のアコーディオンの開閉の動きを作る場合でもCSSを使った実装が必須です。
アクセシビリティ面・ユーザビリティ面
- キーボード操作の対策を行わなければ、タブフォーカスを行えないことが多いです。エンターキーやスペースキーによるアコーディオンの開閉操作もJavaScriptなしではできません。
- スクリーンリーダーの対策を行わなければ、開閉状態を読み上げてくれることはありません。
- サイト内単語検索でアコーディオンの中にある単語が引っかからないことはありがちです。また単語が引っかかったとしてもアコーディオンが開かず、単語を見つけられないことがあります。
アコーディオンUIを作る際に<details>
要素と<summary>
要素を使うメリット
課題がわかったところで、今度はアコーディオンUIで<details>
要素と<summary>
要素を使うメリットを見てみましょう。先にあげた課題点をすべてカバーでき、アクセシビリティ面ではとくに優れていると言えます。
コード面
HTMLの構造がシンプル。タグ名から構造を理解できることがおわかりいただけるでしょう。
<details>
<summary>概要</summary>
コンテンツ詳細
</details>
実装工数の面
必要最低限のアコーディオンの開閉動作を作る場合にはCSSは不要で、HTMLだけで十分です。
アクセシビリティ面・ユーザビリティ面
特別なケアをしなくてもアクセシビリティ面で最適化されています。
-
JavaScriptでキーボードイベントを登録することなく、タブフォーカスとエンターキー・スペースキーでの開閉操作ができます。
-
スクリーンリーダーが開閉状態について適切に読み上げてくれます。たとえばmacOSのVoiceOverを使ってGoogle Chromeを読み上げさせてみましょう。アコーディオンが閉じている状態では「(概要文)下位項目が折りたたまれました、三角形の展開ボタン、グループ」、開いている状態では「(概要文)字間広く、三角形の展開ボタン、グループ」のように開閉状態を判断した内容が読まれます。
-
サイト内で単語検索を行うと、検索した単語の含まれるアコーディオンが開き、中身の単語に直接移動できます。
以上で、<details>
要素と<summary>
要素が最適と言える理由を説明しました。つづいて、STEP1〜3の手順でアコーディオンUIを作成していきましょう。
STEP 1: HTMLでマークアップ
概要文などを入れる<summary>
要素を<details>
要素で囲うと基本的な形ができます。つづいて、<summary>
要素の下に、コンテンツ詳細文などの折りたたませておく部分をマークアップすればアコーディオンが完成です。以下の例では折りたたまれている部分にタグをつけていませんが、この部分にはさまざまなタグが使えます。
▼基本的なHTMLの形
<details>
<summary>概要</summary>
折りたたまれている部分です。
</details>
ブラウザで確認してみましょう。なんとたった2つのタグを使うだけでアコーディオンを開閉できました!
▼基本的なHTMLで作ったアコーディオンの動作
STEP 2: CSSでスタイリング
次にCSSで見た目を変えていきます。以下のようなアイコンを持つ、アコーディオンUIを作っていきます。
- サンプルを別ウインドウで開く
- コードを確認する(HTML、CSS)
まず、アイコンを変えるため表示されているデフォルトの三角形アイコンを消しましょう。初期値のdisplay: list-item
で三角形アイコンが表示されている状態なのでdisplay: block
等に変更します。
summary {
/* display: list-item;以外を指定してデフォルトの三角形アイコンを消します */
display: block;
}
注意点として、Safariだけ-webkit-details-marker
というCSSの疑似要素で三角形アイコンが表示されています。以下のように別途指定して疑似要素を消しましょう。
summary::-webkit-details-marker {
/* Safariで表示されるデフォルトの三角形アイコンを消します */
display: none;
}
つづいて、新しくアイコンを作っていきます。アイコンの作り方は、アイコン用タグを用意する方法とCSS疑似要素で作る2通りの方法が考えられます。
アイコン用タグを追加する場合は以下のように<span>
要素等をHTMLに追加します。アイコン自体のCSSは長くなるのでサンプルCSSの該当箇所をご覧ください。
<details>
<summary>
概要<span class="icon"></span>
</summary>
折りたたまれている部分です。
</details>
CSS疑似要素でアイコンを作る場合は、アニメーションさせる際に注意が必要になるので後述の「Safariの注意点」をご覧ください。
さらに、<summary>
要素のアイコンとテキストのレイアウトを整えていきましょう。今回はdisplay: flex
を使いました。
サンプルCSSでは、iOS 15未満の<summary>
要素でdisplay: flex
が機能しないバグ対策として、.summary_inner
に対してdisplay: flex
を指定してあります。
対応させたい要件に合わせて以下のようなケアを行ったり、新たにタグを追加したくない場合はposition: absolute
でアイコン位置を指定するなど他の方法を検討すると良いでしょう。
▼<summary>
要素に中身を囲むインナー要素を追加したサンプルHTML
<details>
<summary>
<span class="summary_inner">
概要<span class="icon"></span>
</span>
</summary>
折りたたまれている部分です。
</details>
▼アイコンとテキストのレイアウトを調整するサンプルCSS(一部抜粋)
.summary_inner{
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
CSSでアイコンをアニメーションさせる
アイコンだけならJavaScript不要でアニメーションできます。以下のようにdetails[open]
セレクターでアコーディオンが開いたときのスタイルを登録しアイコンをアニメーションさせることが可能です。
.icon {
/* 省略 */
transition: transform 0.4s;
}
/* アコーディオンが開いた時のスタイル */
details[open] .icon {
transform: rotate(180deg);
}
Safariの注意点
2022年9月時点で、Safariのみbefore
・after
といったCSS疑似要素をtransform: rotate()
でトランジションさせようとすると動かなくなるバグがあります。このバグは@keyframes
で作成したアニメーションを開閉時それぞれに呼び出すことで回避できます(サンプルCSSの該当箇所を参照)。ただし、閉じるときのアニメーションがリロード時に一度発火してしまうので、アイコン用タグを用意する方法をオススメします。
STEP 3: JavaScriptでアニメーションをつける
クリックまたはキーボード操作によって、アコーディオンがアニメーションしながら開閉するという動きを作っていきましょう。今回はブラウザー標準機能であるWeb Animations API
を使ってアコーディオンの中身をアニメーションさせます。 アニメーションライブラリ等はお好みのものを使うとよいでしょう。迷ったときは 『現場で使えるアニメーション系JSライブラリまとめ』をご覧ください。
▼今回作る作例
- サンプルを別ウインドウで開く
- コードを確認する(HTML、CSS、JavaScript)
JavaScript実装にあたってHTMLとCSSを少し変えます。まずHTMLにJavaScript操作用のクラスを追加し、折りたたまれている部分(<summary>
要素の下)は二重の<div>
要素で囲うようにしました。
▼中身をアニメーションさせるために変更したHTML
<details class="js-details">
<summary class="js-summary">概要<span class="icon"></span></summary>
<div class="content js-content">
<div class="content_inner">
折りたたまれている部分です。
</div>
</div>
</details>
次に、外側の<div>
要素であるcontent
クラスにCSSでoverflow: hidden
を指定します。そして内側の<div>
要素であるcontent_inner
クラスに対して、padding
を指定します。<details>
タグ直下であるcontent
クラスには上下のpadding
を指定しないことがポイントです。上下padding
を指定するとアニメーションするときにカクついてしまうためです。
▼中身をアニメーションさせるために変更したCSS(一部抜粋)
/* --------アコーディオンの中身のスタイル-------- */
.content {
overflow: hidden;
/* details直下のタグにpaddingを設定すると挙動がおかしくなるので、ここには指定しない */
}
.content_inner {
padding: 24px 48px;
}
アニメーションの登録
HTMLとCSSの準備ができたところで、JavaScriptの実装に入ります。まず、中身のjs-content
を表示・非表示させるアニメーションを登録します。 今回は高さと透明度を変えるシンプルなアニメーションを用意しました。
/**
* アニメーションの時間とイージング
*/
const animTiming = {
duration: 400,
easing: "ease-out"
};
/**
* アコーディオンを閉じるときのキーフレーム
*/
const closingAnimKeyframes = (content) => [
{
height: content.offsetHeight + 'px', // height: "auto"だとうまく計算されないため要素の高さを指定する
opacity: 1,
}, {
height: 0,
opacity: 0,
}
];
/**
* アコーディオンを開くときのキーフレーム
*/
const openingAnimKeyframes = (content) => [
{
height: 0,
opacity: 0,
}, {
height: content.offsetHeight + 'px',
opacity: 1,
}
];
クリックイベントの登録
次に、<summary>
要素に対してクリックイベントを登録します。<details>
要素はopen
属性の有無(論理属性)によってアコーディオンの中身を表示・非表示と切り替えています。<details>
要素のopen
プロパティ(論理値)で開閉状態の判定が可能なので、details.open
で判定して、先ほど登録したアニメーションを実行します。
ところが、<details>
要素からopen
属性が取り除かれると中身は一瞬で非表示になってしまいます。これではいくらアニメーションさせても見えません(display: none
のようなイメージ)。そこで、アニメーションを表示させるため<summary>
要素のクリックイベントにpreventDefault()
メソッドを追加してデフォルトの挙動を無効化します。
また、デフォルトの挙動を無効化すると当然アコーディオンの開閉操作ができなくなってしまうので、手動でopen
属性の切り替えを行います。アコーディオンが開くときはクリック時にopen
属性を付与し、閉じるときはアニメーション完了後にopen
属性を取り除きます。
const details = document.querySelector(".js-details");
const summary = document.querySelector(".js-summary");
summary.addEventListener("click", (event) => {
// デフォルトの挙動を無効化
event.preventDefault();
// detailsのopen属性を判定
if (details.open) {
// アコーディオンを閉じるときの処理
// ...略
// アニメーションを実行
const closingAnim = content.animate(closingAnimKeyframes(content), animTiming);
closingAnim.onfinish = () => {
// アニメーションの完了後にopen属性を取り除く
details.removeAttribute("open");
};
} else {
// アコーディオンを開くときの処理
// open属性を付与
details.setAttribute("open", "true");
// アニメーションを実行
const openingAnim = content.animate(openingAnimKeyframes(content), animTiming);
// ...略
}
});
お気づきの方もいるかもしれませんが、開くときには表示状態になってから中身がアニメーションするので、普通なら手動でopen
属性をつける必要はないはずです。しかし、Safariでアニメーションされないバグがあるため、開くとき・閉じるときどちらに対しても、手動でopen
属性を切り替えるようにしてあります。
▼アニメーションの実行・open
属性の切り替えタイミングは異なる
詳しい解説は省略しますが、アイコンのアニメーションを行うタイミングを管理するため、is-opened
クラスをサンプルコードには追加しています(CSS該当箇所、JavaScript該当箇所)。どのタイミングでアイコンが動作するのかぜひ確認してみてください。
連打対策
これで中身がアニメーションして開閉するようになりました。しかしこのままでは不完全です。連打するとアニメーション実行中に次のアニメーションが始まってしまい、アニメーションの挙動が不安定になります。
そこでアニメーション中に連打しても開閉状態が切り替わらないように対策します。今回は連打防止用のdata-anim-status
というカスタムデータ属性を用意し、値running
でアニメーション状態を管理する処理を追加しました。アニメーション中だけ値を入れておき、値が入っている間はクリックしてもリターンするようにします。JavaScriptではdataset.animStatus
で、用意したカスタムデータ属性への参照が行えます。
▼連打防止用の処理を追加したJavaScript(一部抜粋)
summary.addEventListener("click", (event) => {
// 連打防止用。アニメーション中だったらクリックイベントを受け付けないでリターンする
if (details.dataset.animStatus === "running") {
return;
}
// detailsのopen属性を判定
if (details.open) {
// ...略
// アニメーション実行中用の値を付与
details.dataset.animStatus = "running";
closingAnim.onfinish = () => {
// アニメーションの完了後に値を取り除く
details.dataset.animStatus = "";
};
} else {
// ...略
// アニメーション実行中用の値を付与
details.dataset.animStatus = "running";
openingAnim.onfinish = () => {
// アニメーションの完了後に値を取り除く
details.dataset.animStatus = "";
};
}
});
注意点
<details>
要素にクリックイベントを追加すると、開いた中身の部分もクリックできてしまいます。クリックイベントは<summary>
要素に対して追加するようにしましょう。
おまけ:連打対応バージョン
アコーディオンの開閉アニメーション途中でもクリックできる、連打対応バージョンも用意しました。連打してみると挙動の違いがわかるでしょう。詳しくはデモとサンプルコードをご覧ください。作成にはJavaScriptアニメーションライブラリGSAPを使っています。
まとめ
アコーディオン型UIを<details>
要素と<summary>
要素で作ると得られるメリットから、詳しい作り方までを紹介しました。
実装するデザインに応じて、以下のように段階を踏んで手を加えてくとよいでしょう。
- とりあえずアコーディオンUIをお手軽に作りたい→HTMLだけで
- アコーディオンUIの見た目にこだわって作りたい→HTML・CSSで
- アコーディオンUIの見た目も動きもこだわって作りたい→HTML・CSS・JavaScriptで
アコーディオンUIを作る際のさまざまな課題ケアには、<details>
要素と<summary>
要素が最適です。ぜひ使っていきましょう。
参照記事
※この記事が公開されたのは2年前ですが、1年前の2023年5月に内容をメンテナンスしています。