昔はアクセシビリティに配慮してアコーディオンの開閉機能を実装するにはJavaScriptが必須でしたが、HTML5で導入されたdetailsタグとsummaryタグを使えば、JavaScriptなしでアクセシブルなアコーディオンを実装できるようになりました。
このページでは、そのdetails/summaryタグを用いてアコーディオンを実装する方法を紹介します。
また、開閉時のアニメーションをつける方法についても、以下の実装例についてそれぞれ解説します。
- JSを使わずにCSSのみでアニメーションを実装する
- JSを併用し、grid-template-rowsをアニメーションさせる
- JSを併用し、heightをアニメーションさせる
- (おまけ) jQueryを使ってアニメーションを実装する
details/summary要素についての基礎知識
まずは基本的な情報をおさらいしておきます。
details/summary要素には開閉機能がネイティブで備わっており、JavaScriptが必要ありません。
また、CSSでスタイリングしなくてもデフォルトで左側に三角形のマーカー(▶)が表示され、展開時には(▼へと)方向が変わるようになっており、HTMLだけでも最低限の機能と見た目が担保されています。
まずはシンプルな例を見てみましょう。
クリックして詳細を表示できます
ここに詳細な内容が入ります。
<summary>クリックして詳細を表示できます</summary>
これだけで最低限の機能を持つアコーディオンが実装できます。
※ summary要素は、details要素の最初の子要素として配置する必要があります。
details/summary を使うメリット
- 開閉機能がネイティブで備わっている。
- Tabキーでフォーカス移動、Enter/Spaceキーで開閉が可能。
- スクリーンリーダー対応で、「折りたたみ」「展開済み」などの状態が自動的に通知される。
- ページ内テキスト検索時に、コンテンツが隠れていても自動で展開してくれる。
- name属性を使うだけで、展開する要素を一つに制限してくれる。
独自にdiv要素やbutton要素でアコーディオンを実装する場合、これらのアクセシビリティ対応を自前で書く必要があり、JavaScriptも必要になってきます。
details/summaryを使えばその手間が省けるので、より楽に実装できます。
[open]属性について
details要素が開いている状態では、HTML上にopen属性が自動的に付与されます。これを属性セレクタで参照することで、開閉状態に応じたスタイルを適用できます。
/* details要素が開いている時のスタイル */
逆に、折りたたまれる時にはこのopen属性が自動的に削除されます。
open属性が外れている時、コンテンツはレンダリングから除外されています(content-visibility: hiddenな状態)。
クリック時には、e.preventDefault() でこのopen属性の自動付け外しを無効化することもできますが、ページ内検索時や、name属性による自動折りたたみ時に発火するのはopen属性の操作が終わった直後に発火するtoggleイベントとなることに注意が必要です。
デフォルトの三角形マーカーを非表示にする方法
summary要素の三角形マーカーは、Chrome/Edge/Firefoxなどでは、summary要素のdisplayプロパティをlist-item以外に変更することで自動的に非表示になります。
Safariでは、::-webkit-details-markerという独自の疑似要素でこのマーカーがスタイリングされているため、以下のようにして明示的に非表示にするスタイルを書く必要があります。
summary::-webkit-details-marker {
summaryタグのアクセシビリティに関する問題点
一部のスクリーンリーダーとブラウザの組み合わせによっては、summary内の見出しロールが検出できない問題があるようです。
これはsummaryが暗黙的にrole="button"扱いになっていることが原因のようです。
HTMLの仕様としては summary > hタグ の構造は許可されているものの、一部の環境ではスクリーンリーダーでは正しく反映されない状況となっています。
今はほとんど解決してるっぽい…??
この問題について、2026年1月に色々試してみましたが、おそらくですがほとんど解消してるのではないかと思います。
Safari + VoiceOver の組み合わせで特に問題が多かったようですが、MacOS 15.7 / iOS 26.1 で確認した限りではローターの見出しリストにもちゃんと出て来ました。
まずはシンプルに実装してみる
それでは、実際にdetailsとsummaryを使ってアコーディオンを実装してみましょう。
まずは、開閉時のアニメーションはなしにして、アイコン部分だけ軽くアニメーションさせてみます。
@import url(../_common.css);
/* 開いている時 */
.accordion[open]{
--icon-transform: rotate(90deg);
}
/* コンテンツ部分の padding */
.accordion__body {
padding: 0.5rem 0.75rem 1.25rem;
}
<div class="accordions">
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 01</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 01 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 02</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 02 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 03</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 03 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
</div>
/* ----- 各デモで共通するスタイル部分をまとめたファイル ----- */
.accordions {
--bdc: color-mix(in srgb, currentColor, transparent 70%);
border-block: solid 1px var(--bdc);
}
.accordion {
--duration: 0.25s; /* transition時間 */
--icon-transform: rotate(0deg); /* 開閉時にiconを変化させるための変数 */
}
/* 2つ目以降のアコーディオンには上部にボーダーを追加 */
.accordion + .accordion {
border-top: 1px solid var(--bdc);
}
.accordion__title {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 1.25rem 0.75rem;
cursor: pointer;
}
/* Safariで表示されるデフォルトの三角形アイコンを削除 */
.accordion__title::-webkit-details-marker {
display: none;
}
/* 見出しタグでもフォントを揃える */
.accordion__title > :where(:not(.accordion__icon)) {
font: inherit;
}
/* コンテンツ間の余白*/
.flow > * + * {
margin-block-start: 0.5rem;
}
/* タイトルホバー時、背景色を少し濃くする */
@media (any-hover: hover) {
.accordion__title:hover {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
}
/* Tabキーでのフォーカス時も同じスタイリング */
.accordion__title:focus-visible {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
/* アイコン */
.accordion__icon {
display: grid;
width: 1em;
height: 1em;
place-items: center;
flex-shrink: 0;
&::before,
&::after {
content: '';
display: block;
grid-area: 1 / 1;
background-color: currentColor;
}
&::before {
width: 0.1em;
height: 100%;
transition: transform var(--duration);
transform: var(--icon-transform);
}
&::after {
height: 0.1em;
width: 100%;
}
}
/* jsと併用する例にのみ共通 */
.accordion[data-opened] {
--icon-transform: rotate(90deg);
}
HTMLの構造はシンプルです。.accordionsというラッパー要素の中に、複数のdetails要素を配置しています。各details要素にはsummaryでタイトルを、その後に続くdiv要素の中ではコンテンツを配置しています。
アニメーションをつけなくて良い場合は、なんとこれだけでアコーディオンが実装できるようになっています。
コンテンツの開閉アニメーション
アコーディオンUIを実装する際、コンテンツ部分の開閉にスライドアニメーションを付けるケースが多いですが、これまで、CSSだけではdetailsの開閉アニメーションを実装することは困難でした。
従来の問題は次の2点です。
height: auto ではアニメーションができない。
open属性が外れている状態からのアニメーションができない。(display:none からフェードインできないのと同じ)
しかしながら、最近ではCSSだけでもアニメーションが可能になりつつあります。
- 1を解決するTips :
grid-template-rowsを0fr~1frへトランジションさせるというテクニックがあります。また、将来的にはinterpolate-size: allow-keywords;を使うことでheight:autoのアニメーションが可能になります。
- 2を解決するTips :
::details-content擬似要素を使うことで、コンテンツ用のslot要素に対してスタイルを付与することができるようになり、アニメーションも可能になりました。
interpolate-size, ::details-contentはまだブラウザ対応が完全ではないため積極的に導入はしずらいですが、将来的にはこれらを使ってCSSだけで開閉アニメーションを実装できるようになります。
ブラウザサポート状況(2025年12月時点)
Can Use ? interpolate-size
129 (2024/9)~
Can Use ? ::details-content
131 (2024/11)~
18.4 (2025/3)~
143 (2025/9)~
これを見ると、::details-contentは主要ブラウザで広くサポートされはじめていることがわかります。
アニメーション実装例1: CSSだけで開閉アニメーションを実装する例
::details-content に対して 0fr~1frのトランジションTispを使い、CSSだけで開閉アニメーションを実装する例を紹介します。
@import url(../_common.css);
/* ::details-content をサポートしていないブラウザではスタイルが崩れないようにアニメーション関連のみにする */
.accordion::details-content {
display: grid;
transition-duration: var(--duration);
transition-property: content-visibility, grid-template;
transition-behavior: allow-discrete;
/* 閉じてる状態 */
grid-template-rows: 0fr;
}
/* 開いている時 */
.accordion[open]{
--icon-transform: rotate(90deg);
}
.accordion[open]::details-content {
grid-template-rows: 1fr;
}
.accordion__body {
/* grid-template のアニメーションに必要 */
overflow: hidden;
}
.accordion__content {
padding: 0.5rem 0.75rem 1.25rem;
}
<div class="accordions">
<details class="accordion" name="demo">
<summary class="accordion__title">
<h3>アコーディオン 01</h3>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body">
<div class="accordion__content flow">
<p>アコーディオン 01 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</div>
</details>
<details class="accordion" name="demo">
<summary class="accordion__title">
<h3>アコーディオン 02</h3>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body">
<div class="accordion__content flow">
<p>アコーディオン 02 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</div>
</details>
<details class="accordion" name="demo">
<summary class="accordion__title">
<h3>アコーディオン 03</h3>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body">
<div class="accordion__content flow">
<p>アコーディオン 03 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</div>
</details>
</div>
/* ----- 各デモで共通するスタイル部分をまとめたファイル ----- */
.accordions {
--bdc: color-mix(in srgb, currentColor, transparent 70%);
border-block: solid 1px var(--bdc);
}
.accordion {
--duration: 0.25s; /* transition時間 */
--icon-transform: rotate(0deg); /* 開閉時にiconを変化させるための変数 */
}
/* 2つ目以降のアコーディオンには上部にボーダーを追加 */
.accordion + .accordion {
border-top: 1px solid var(--bdc);
}
.accordion__title {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 1.25rem 0.75rem;
cursor: pointer;
}
/* Safariで表示されるデフォルトの三角形アイコンを削除 */
.accordion__title::-webkit-details-marker {
display: none;
}
/* 見出しタグでもフォントを揃える */
.accordion__title > :where(:not(.accordion__icon)) {
font: inherit;
}
/* コンテンツ間の余白*/
.flow > * + * {
margin-block-start: 0.5rem;
}
/* タイトルホバー時、背景色を少し濃くする */
@media (any-hover: hover) {
.accordion__title:hover {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
}
/* Tabキーでのフォーカス時も同じスタイリング */
.accordion__title:focus-visible {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
/* アイコン */
.accordion__icon {
display: grid;
width: 1em;
height: 1em;
place-items: center;
flex-shrink: 0;
&::before,
&::after {
content: '';
display: block;
grid-area: 1 / 1;
background-color: currentColor;
}
&::before {
width: 0.1em;
height: 100%;
transition: transform var(--duration);
transform: var(--icon-transform);
}
&::after {
height: 0.1em;
width: 100%;
}
}
/* jsと併用する例にのみ共通 */
.accordion[data-opened] {
--icon-transform: rotate(90deg);
}
ポイント
::details-contentに対してgrid-template-rowsを0fr~1frへトランジションさせる.
transition-property: content-visibility & transition-behavior: allow-discreteの指定を忘れないように.
- また、その子要素に対して
overflow: hiddenが必要なので、div(.accordion__body)でラップする.
::details-contentを使うのは、まだあくまでアニメーション部分だけ(非サポートブラウザではただアニメーションがなくなるだけですむように。)
- コンテンツ上下に
paddingをつける時は、さらにネストを重ねたコンテンツラッパーの.accordion__contentにつける。(もしくは、.accordion__bodyの擬似要素を使ってスペースを確保する。)
Can I use でcontent-visibility: Transitionable when setting transition-behavior: allow-discreteを確認するとFirefoxではサポートされていないことになっていますが、手元のFirefox v.145.0.2 では動作していました。
コンテンツ上下のpadding/marginの付け方について
上記では、コンテンツの余白(padding)をつけるために、.accordion__bodyの直下でさらに.accordion__contentを配置しています。
これはなんでかというと、.accordion__bodyに上下方向の余白(paddingやmargin)をつけると、アニメーションが一瞬ちらつくためです。
元々、grid-template-rows を 0fr → 1fr へアニメーションさせる時、その要素またはその直下要素に対して上下方向の余白があると、正常に動作しません。
それ自体は、次のようにpadding-blockをtransitionでアニメーションさせることで回避できます。
/* Memo: .accordion__contentの上下にpaddingをつける場合 */
transition: padding-block var(--duration);
transition-duration: var(--duration);
[open] > .accordion__content {
ただし、::details-contentを使っている場合は、このようにアニメーションさせて対策しても、初回アニメーション時にのみ、1フレームだけちらつきが発生してしまうようです。(Mac Chromeで確認)
なので、::details-contentを0fr→1frでアニメーションさせる場合は、余白をつけるためにさらに階層を深くするか、擬似要素やスペーサー要素を使って余白を確保することでその問題を回避する必要があります。
アニメーション実装例2: JSを併用し、grid-template-rowsをアニメーションさせる
Safariもサポートしつつアニメーションを実装するなら、JSの力を借りましょう。
スライドアニメーションは先ほどと同じ0fr~1frへのトランジションを使いますが、アニメーショントリガー用のdata-opened属性をJSでつけ外しするような実装をする例を紹介します。
ポイントは、open属性の切り替わりとはタイミングをずらして独自のdata属性をつけ外しすることです。
@import url(../_common.css);
.accordion__body {
display: grid;
transition-property: padding-block, grid-template;
transition-duration: var(--duration);
/* 閉じてる状態 */
grid-template-rows: 0fr;
padding-block: 0;
padding-inline: 0.75rem;
}
/* 開いている時 */
[data-opened] > .accordion__body {
grid-template-rows: 1fr;
padding-block: .5rem 1.25rem;
}
/* grid アニメーションに 必須 */
.accordion__content {
overflow: hidden;
}
<div class="accordions" data-multiple="disallow">
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 01</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body">
<div class="accordion__content flow">
<p>アコーディオン 01 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 02</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body">
<div class="accordion__content flow">
<p>アコーディオン 02 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 03</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body">
<div class="accordion__content flow">
<p>アコーディオン 03 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</div>
</details>
</div>
// アニメーションが完了するのを待つ
const waitAnimation = (details) => {
// アニメーション対象の要素を直接取得.(getAnimations({subtree: true}) は iOS Safari で動作しない場合があるので __body を直接監視)
const body = details.querySelector('.accordion__body');
const animations = body ? body.getAnimations() : [];
// allSettled を使うことで、キャンセル時も reject せずに完了する
return Promise.allSettled(animations.map((a) => a.finished));
};
const open = async (details) => {
// すでに開いている場合は何もしない
if (details.open && details.hasAttribute('data-opened')) return;
// open属性をセット
details.open = true;
// 次フレームで data-opened 属性を付与(CSS側でフェードインアニメーション開始)
requestAnimationFrame(() => {
details.setAttribute('data-opened', ''); // 属性の追加
});
};
const close = async (details) => {
// すでに閉じている場合は何もしない
if (!details.open && !details.hasAttribute('data-opened')) return;
details.removeAttribute('data-opened'); // 属性を削除
// アニメーションを待つ
await waitAnimation(details);
// アニメーション完了後にopen属性 を除去。
details.open = false;
};
const setEvent = (details) => {
// summary 要素をトリガーとする
const trigger = details.querySelector('summary');
if (!trigger) return;
// 親要素を取得
const parent = details.parentNode;
// 複数展開を許可するかどうかを、親要素の [data-multiple] でチェックする。(name属性での開閉はアニメーションが効かない)
let allowMultiple = false;
if (null != parent) {
allowMultiple = 'disallow' !== parent.getAttribute('data-multiple');
}
// summary の 'click' イベント
trigger.addEventListener('click', onClick);
function onClick(e) {
// すぐに open 属性が切り替わらないようにする
e.preventDefault();
// 開く処理
if (!details.open) {
// (複数展開が禁止されている場合)他の開いているアイテムがあるかどうかを先に探して閉じる
if (!allowMultiple) {
const openedItem = parent.querySelector(`[data-opened]`);
requestAnimationFrame(() => {
// 1フレーム待機(safariでは requestAnimationFrame() がないと動かなかった)
if (null != openedItem) close(openedItem);
});
}
open(details);
}
// 閉じる処理
else if (details.open) {
close(details);
}
}
// ページ内検索時にopenだけ付与されてしまうので、'toggle'イベントで属性操作する
// toggleイベントはclick時やname属性による開閉処理でも発火することに注意。また、e.preventDefault() は効かない。)
details.addEventListener('toggle', onToggle);
function onToggle(e) {
const hasOpen = details.open;
const hasDataOpen = details.hasAttribute('data-opened');
// open はセットされたのに data-opened 属性がついてない時
if (hasOpen && !hasDataOpen) {
details.setAttribute('data-opened', '');
}
// open は削除されたのに data-opened 属性がまだついている時 (name属性が使われた時用)
if (!hasOpen && hasDataOpen) {
details.removeAttribute('data-opened');
}
}
};
/**
* 処理の実行
*/
(function () {
const detailsAll = document.querySelectorAll('.accordion');
detailsAll.forEach((details) => {
setEvent(details);
});
})();
/* ----- 各デモで共通するスタイル部分をまとめたファイル ----- */
.accordions {
--bdc: color-mix(in srgb, currentColor, transparent 70%);
border-block: solid 1px var(--bdc);
}
.accordion {
--duration: 0.25s; /* transition時間 */
--icon-transform: rotate(0deg); /* 開閉時にiconを変化させるための変数 */
}
/* 2つ目以降のアコーディオンには上部にボーダーを追加 */
.accordion + .accordion {
border-top: 1px solid var(--bdc);
}
.accordion__title {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 1.25rem 0.75rem;
cursor: pointer;
}
/* Safariで表示されるデフォルトの三角形アイコンを削除 */
.accordion__title::-webkit-details-marker {
display: none;
}
/* 見出しタグでもフォントを揃える */
.accordion__title > :where(:not(.accordion__icon)) {
font: inherit;
}
/* コンテンツ間の余白*/
.flow > * + * {
margin-block-start: 0.5rem;
}
/* タイトルホバー時、背景色を少し濃くする */
@media (any-hover: hover) {
.accordion__title:hover {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
}
/* Tabキーでのフォーカス時も同じスタイリング */
.accordion__title:focus-visible {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
/* アイコン */
.accordion__icon {
display: grid;
width: 1em;
height: 1em;
place-items: center;
flex-shrink: 0;
&::before,
&::after {
content: '';
display: block;
grid-area: 1 / 1;
background-color: currentColor;
}
&::before {
width: 0.1em;
height: 100%;
transition: transform var(--duration);
transform: var(--icon-transform);
}
&::after {
height: 0.1em;
width: 100%;
}
}
/* jsと併用する例にのみ共通 */
.accordion[data-opened] {
--icon-transform: rotate(90deg);
}
この実装の問題点
name属性を使って同一グループ内でのアコーディオン開閉を一つに制限すると、アニメーションがうまく動作しなくなります。
これについては、上の例では、data-multipleという属性を使って独自実装することで対応しています。
また、iframeの埋め込み内だとページ内検索時の展開アニメーションが動作しないように見えますが、別タブで確認すると問題なくアニメーションします。
アニメーション実装例3: JSを併用し、heightをアニメーションさせる(連打可能)
最後に、heightをアニメーションさせる実装例を紹介します。
先ほどと同様、jsでdata-opened属性を付け外ししながら、heightを取得してそれをCSS変数へセットします。
アニメーション自体は、CSS側でheightのtransitionで行います。
heightでアニメーションさせることで、クリックを連打される(開いている途中にクリックされたりする)場合でも、すぐにその地点のから逆方向へ動作するようにしています。
@import url(../_common.css);
.accordion {
overflow: hidden; /* アニメーション中にコンテンツがはみ出ないように */
position: relative;
height: var(--js--height);
transition: height var(--duration);
/* border や padding があるときに高さがズレるのを防ぐ */
box-sizing: content-box;
/* jsで変数にセットする */
--js--height: auto;
}
.accordion__body {
padding: .5rem .75rem 1.25rem;
}
/* パネルが完全に閉じている時、absoluteで飛ばしておくとページ内検索の挙動が少し良くなる (なぜかはよくわかってない) */
.accordion:not([data-opened]) > .accordion__body{
position: absolute;
}
/* フォーカス時のアウトラインを少し内側に寄せる( overflow:hidden によって途切れてしまうので) */
.accordion__title:focus-visible {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
/* offset 調整だけだとブラウザで差が大きいので、スタイルまで指定してしまう */
outline: solid 2px currentColor;
outline-color: revert;
outline-offset: -3px;
}
<div class="accordions" data-multiple="disallow">
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 01</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 01 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 02</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 02 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 03</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 03 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
</div>
// アニメーションが完了するのを待つ(正常終了したかどうかを返す)
const waitAnimation = async (details) => {
const animations = details.getAnimations();
// アニメーションがなければ正常終了扱い
if (animations.length === 0) return true;
// allSettled を使うことで、キャンセル時も例外にならずに結果を取得できる
const results = await Promise.allSettled(animations.map((a) => a.finished));
// ( pause()で止めた時用 )rejected があれば false
return results.every((r) => r.status === 'fulfilled');
};
// 実行中のアニメーションがあれば一旦停止させる
const pauseAnimation = (details) => {
const animations = details.getAnimations();
if (animations.length > 0) {
animations.forEach((a) => a.pause());
}
};
// open属性がセットされた直後で呼び出す処理
const open = async (details) => {
// 閉じる途中であればアニメーションを一旦停止。
pauseAnimation(details);
// 複数展開を制限するかどうかを、親要素の [data-multiple] でチェックする。
const parent = details.parentNode;
if (parent && 'disallow' === parent.getAttribute('data-multiple')) {
// 他の開いているアイテムを先に探して閉じる
const openedItems = parent.querySelectorAll(`[data-opened]`);
openedItems.forEach((openedItem) => {
if (openedItem === details) return; // 自分自身はスキップ
close(openedItem);
});
}
// 要素取得
const title = details.querySelector('.accordion__title');
const body = details.querySelector('.accordion__body');
// 現在の高さをセット
details.style.setProperty('--js--height', `${details.offsetHeight}px`);
// 次フレームで開くアニメーション開始
requestAnimationFrame(async () => {
// 目標の高さをセットしてdata属性付与
details.style.setProperty('--js--height', `${title.offsetHeight + body.offsetHeight}px`);
details.setAttribute('data-opened', '');
// アニメーションを待つ
const isFinished = await waitAnimation(details);
// 正常に完了したら、リサイズ時を考慮して --js--heigh を消しておく
if (isFinished) details.style.removeProperty('--js--height');
});
};
const close = async (details) => {
// 開く途中であればアニメーションを一旦停止。
pauseAnimation(details);
// 要素取得
const title = details.querySelector('.accordion__title');
// 現在の(開いている状態の)高さをセット
details.style.setProperty('--js--height', `${details.offsetHeight}px`);
// 次のフレームで 閉じるアニメーション開始
requestAnimationFrame(async () => {
// どこまで閉じるかの高さをセット
details.style.setProperty('--js--height', `${title.offsetHeight}px`);
details.removeAttribute('data-opened');
// アニメーションを待つ
const isFinished = await waitAnimation(details);
// 正常に完了したら
if (isFinished) {
// open属性 を削除
details.open = false;
// リサイズ時を考慮して --js--heigh を消しておく
details.style.removeProperty('--js--height');
}
});
};
const setEvent = (details) => {
// summary 要素をトリガーとする
const trigger = details.querySelector('summary');
if (!trigger) return;
// summary の 'click' イベント
trigger.addEventListener('click', function (e) {
// すぐに open 属性が切り替わらないようにする
e.preventDefault();
// 開く処理
if (!details.hasAttribute('data-opened')) {
// まずopen属性をセット
details.open = true;
open(details);
}
// 閉じる処理
else {
close(details);
}
});
// ページ内検索時に open だけ付与されてしまうので、'toggle'イベントでも属性操作する
// toggleイベントはclick時やname属性による開閉処理でも発火することに注意。また、e.preventDefault() は効かない。)
details.addEventListener('toggle', function (e) {
const hasOpen = details.open;
requestAnimationFrame(() => {
const hasDataOpen = details.hasAttribute('data-opened');
// open はセットされたのに data-opened 属性がついてない時
if (hasOpen && !hasDataOpen) {
open(details);
}
// open は削除されたのに data-opened 属性がまだついている時 (name属性が使われた時用)
if (!hasOpen && hasDataOpen) {
details.removeAttribute('data-opened');
}
});
});
};
/**
* 処理の実行
*/
(function () {
const detailsAll = document.querySelectorAll('.accordion');
detailsAll.forEach((details) => {
setEvent(details);
});
})();
/* ----- 各デモで共通するスタイル部分をまとめたファイル ----- */
.accordions {
--bdc: color-mix(in srgb, currentColor, transparent 70%);
border-block: solid 1px var(--bdc);
}
.accordion {
--duration: 0.25s; /* transition時間 */
--icon-transform: rotate(0deg); /* 開閉時にiconを変化させるための変数 */
}
/* 2つ目以降のアコーディオンには上部にボーダーを追加 */
.accordion + .accordion {
border-top: 1px solid var(--bdc);
}
.accordion__title {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 1.25rem 0.75rem;
cursor: pointer;
}
/* Safariで表示されるデフォルトの三角形アイコンを削除 */
.accordion__title::-webkit-details-marker {
display: none;
}
/* 見出しタグでもフォントを揃える */
.accordion__title > :where(:not(.accordion__icon)) {
font: inherit;
}
/* コンテンツ間の余白*/
.flow > * + * {
margin-block-start: 0.5rem;
}
/* タイトルホバー時、背景色を少し濃くする */
@media (any-hover: hover) {
.accordion__title:hover {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
}
/* Tabキーでのフォーカス時も同じスタイリング */
.accordion__title:focus-visible {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
/* アイコン */
.accordion__icon {
display: grid;
width: 1em;
height: 1em;
place-items: center;
flex-shrink: 0;
&::before,
&::after {
content: '';
display: block;
grid-area: 1 / 1;
background-color: currentColor;
}
&::before {
width: 0.1em;
height: 100%;
transition: transform var(--duration);
transform: var(--icon-transform);
}
&::after {
height: 0.1em;
width: 100%;
}
}
/* jsと併用する例にのみ共通 */
.accordion[data-opened] {
--icon-transform: rotate(90deg);
}
grid-template-rowsの例と今回のheightの例を比べると、少し動きのニュアンスに違いがあることに気づきますでしょうか。
アニメーション実装例2 ではpaddingも同時に操作していたので、動きが少し異なります。
(grid-template-rowsの場合もさらに階層深くするか擬似要素でスペース確保すれば同様の動きになります。)
この実装の問題点
name属性を使って同一グループ内でのアコーディオン開閉を一つに制限すると、アニメーションがうまく動作しなくなります。
これついては アニメーション実装例2 と同じように、独自に用意した data-multiple 属性を使って対応しています。
(おまけ)jQuery を使ってアニメーションを実装する例
最後に、一応軽くjQuery実装パターンも紹介しておきます。
どうしても要件的にjQueryを使わなければならないような場合のみ、参考にしてみてください。
個人的には、アコーディオンのためだけにjQueryを導入するなんてことは非推奨です。(slideUp/slideDownは便利ですけど…。)
@import url(../_common.css);
.accordion__body {
padding: 0.5rem 0.75rem 1.25rem;
}
<div class="accordions">
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 01</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 01 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 02</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 02 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
<details class="accordion">
<summary class="accordion__title">
<span>アコーディオン 03</span>
<span class="accordion__icon" aria-hidden="true"></span>
</summary>
<div class="accordion__body flow">
<p>アコーディオン 03 のコンテンツです。</p>
<p>
ロレム・イプサムの座り雨。目まぐるしい文章の流れの中で、それは静かに歩く仮の言葉です。Elitも穏やかに続いていきますが、積み重ねられてきた「LiberroyとFoogの取り組み」は、余白のようなものです。
</p>
</div>
</details>
</div>
document.addEventListener('DOMContentLoaded', function () {
let accordionDetails = '.accordion';
let accordionSummary = '.accordion__title';
let accordionBody = '.accordion__body';
// --duration を読み取ってミリ秒に変換する関数
function getDuration(element) {
let duration = getComputedStyle(element).getPropertyValue('--duration').trim();
if (duration.endsWith('ms')) {
return parseFloat(duration);
} else if (duration.endsWith('s')) {
return parseFloat(duration) * 1000;
}
return 200; // フォールバック
}
$(accordionDetails).each(function () {
let $details = $(this);
let $summary = $details.find(accordionSummary);
let $content = $details.find(accordionBody);
let speed = getDuration(this);
$summary.on('click', function (event) {
// デフォルトの挙動を無効化する
event.preventDefault();
if ($details.attr('open')) {
// アコーディオンを閉じる処理
$details.removeAttr('data-opened');
$content.slideUp(speed, function () {
// アニメーションの完了後、open属性を取り除く
$details.removeAttr('open');
// ページ内検索にヒットするようにする
$content.show();
});
} else {
// アコーディオンを開く処理
// open属性を付ける
$details.attr('open', 'true');
$details.attr('data-opened', '');
// openがついたらすぐにいったん非表示にしてから開く
$content.hide().slideDown(speed);
}
});
});
});
/* ----- 各デモで共通するスタイル部分をまとめたファイル ----- */
.accordions {
--bdc: color-mix(in srgb, currentColor, transparent 70%);
border-block: solid 1px var(--bdc);
}
.accordion {
--duration: 0.25s; /* transition時間 */
--icon-transform: rotate(0deg); /* 開閉時にiconを変化させるための変数 */
}
/* 2つ目以降のアコーディオンには上部にボーダーを追加 */
.accordion + .accordion {
border-top: 1px solid var(--bdc);
}
.accordion__title {
display: flex;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 1.25rem 0.75rem;
cursor: pointer;
}
/* Safariで表示されるデフォルトの三角形アイコンを削除 */
.accordion__title::-webkit-details-marker {
display: none;
}
/* 見出しタグでもフォントを揃える */
.accordion__title > :where(:not(.accordion__icon)) {
font: inherit;
}
/* コンテンツ間の余白*/
.flow > * + * {
margin-block-start: 0.5rem;
}
/* タイトルホバー時、背景色を少し濃くする */
@media (any-hover: hover) {
.accordion__title:hover {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
}
/* Tabキーでのフォーカス時も同じスタイリング */
.accordion__title:focus-visible {
background-color: color-mix(in srgb, var(--bdc), transparent 80%);
}
/* アイコン */
.accordion__icon {
display: grid;
width: 1em;
height: 1em;
place-items: center;
flex-shrink: 0;
&::before,
&::after {
content: '';
display: block;
grid-area: 1 / 1;
background-color: currentColor;
}
&::before {
width: 0.1em;
height: 100%;
transition: transform var(--duration);
transform: var(--icon-transform);
}
&::after {
height: 0.1em;
width: 100%;
}
}
/* jsと併用する例にのみ共通 */
.accordion[data-opened] {
--icon-transform: rotate(90deg);
}
- 他の実装例ではちゃんとしていた、data-multiple属性の独自実装などはこのjQueryの例ではしていません。
- jQueryのこの実装では、ページ内検索時の展開アニメーションは動作しません。
まとめ
details/summary要素を使えば、開閉機能・キーボード操作・スクリーンリーダー対応・ページ内検索時の自動展開といったアクセシビリティ要件を、JavaScriptなしで満たせます。
開閉時のアニメーションについては、::details-contentとgrid-template-rows: 0fr → 1frのトランジションを使えば、CSSだけでも実装できるようになってきています。
さらに将来的にはinterpolate-sizeによる height:auto のアニメーションのブラウザサポートも採用できるようになるでしょう。
ただし、より広いブラウザ対応が必要な場合は、現在はまだJavaScriptとの併用が現実的です。
アニメーションはなくても困るものではないため、無理に広範囲のブラウザをサポートする必要はなく、CSSだけで実装してモダンブラウザのみアニメーションされる状態にしておくのも許容範囲だと個人的には思います。
参考リンク
この記事を執筆するにあたり、いろんな記事を参考にさせていただきました。
CSSでheight: autoでもアニメーションが可能に! interpolate-sizeとは - ICS MEDIA UIのインタラクションの実装で、height: 0 → autoなど、数値とキーワード値とをアニメーションさせたいと思ったことはないでしょうか。一見可能そうに見えるものの、従来はCSSのみではアニメーションが不可能でした。
ICS MEDIA
detailsとsummaryタグで作るアコーディオンUI - アニメーションのより良い実装方法 - ICS MEDIA アコーディオン型ユーザーインターフェイス(UI)はウェブページでよくみられる表現です。巷ではさまざまな方法でアコーディオンUIを作る方法が紹介されていますが、みなさんはどのような方法で実装していますか。
ICS MEDIA
CSSのみでdetails要素のアニメーションを実装する方法 – TAKLOG
www.tak-dcxi.com
details要素に命を吹き込む::details-content擬似要素の登場 details要素のコンテンツ部分にスタイルを適用できる::details-content擬似要素がBaseline 2025に加わりました。details要素を用いたスタイリングの守備範囲が広がっただけではなく、アニメーションの適用も容易になりました。
k8o
アクセシブルなアコーディオンの実装について考える
Zenn
【jQuery】details要素・summary要素でアコーディオン【メモ】 | deep-space.blue デモはこちら 実装 HTML(ベースの構造と動作) <details class="c-accordion js-accordion"> <summary class="c-accordion__title js-acco
deep-space.blue