はじめに
最近、アコーディオンUIと言えば、details
summary
を使った実装がアクセシビリティもバッチリで良いと聞きます。
しかしながら、全ての状況においてこの実装が適しているのかというとそうとも言えないんじゃないか、とふと思いました。
MDNの例や、このタグを紹介されているページなどを見ると、Q&Aや、何かのリストをアコーディオンに格納するために使っているものが多く、実際そのような使い方には特に抵抗感はありません。
しかし、「ページが長くなるから」などのデザイン的意図を含んだ理由で、section
で囲われるような大きなコンテンツをまるっとアコーディオンの中に格納する場合などは、details(詳細)とsummary(概要)という英語から想像するに、適していないんじゃないか?と感じてしまいます。
<section>
<h2>許されざる呪文</h2>
<p>
死の呪い、磔の呪い、服従の呪文を指す言葉です。<br>
これらの呪いは1717年に使用が禁止され、服従の呪文の支配下だった場合を除いて、終身刑という厳しい処罰が課されました。
</p>
</section>
<details>
<summary>豆知識</summary>
<section>
<h3>許されざる呪文は“合法”だった</h3>
<p>
これら3つの呪文は、1717年に使用禁止となりました。<br>
逆に言えば、それまでは使用されていたようで、決闘時などは人気の呪いだったようです。<br>
1970年代の第一次魔法戦争時は、死喰い人が使用しており、これに対抗するため闇祓いは死喰い人に対する使用を許可した特例も存在しました。<br>
その後、第二次魔法戦争で魔法省が陥落した際も、戦争が終結するまでは合法化されたことがあります。
</p>
</section>
<section>
<h3>3つの呪文を経験し生還した魔法使い</h3>
<p>
これら3つの呪文をかけられ、反対呪文が存在しないはずの死の呪文も2回も生き延びた魔法使いが、ハリー・ポッターです。<br>
また、服従の呪文に関しては対抗する術さえ身に着けています。
</p>
</section>
</details>
See the Pen Accordion Vanilla by heeroo-ymsw (@heeroo-ymsw) on CodePen.
もし、そんなこと気にしなくて良いなら、ぜひ積極的にこのタグを使っていきたいので、調査してみます。
調査
結論
- アコーディオンUIを作りたいときは、多くの場合
details
/summary
で問題ない - ただし、折りたたまれた状態だと、格納した要素がスクリーンリーダーの目次には表示されないことを留意する
エビデンス探し
details
に含めることができるコンテンツの制約
では、MDNを見ていきます。
<details>
: 詳細折りたたみ要素
<details>
は HTML の要素で、ウィジェットが「開いた」状態になった時のみ情報が表示される折りたたみウィジェットを作成します。概要やラベルは<summary>
要素を使用して提供する必要があります。
この「折りたたみウィジェット」の制約については、何も記載がありませんでした。
許可されているコンテンツを見ると、「1つのsummary
要素と、それに続くフローコンテンツ」とあり、こちらもsummary
を入れること以外にほとんど制約はないようです。
よって、HTML要素的には、details
の中にbody
配下に入れるような一般的なタグなら何を突っ込もうが問題はないようです。
ARIAロール
では、ARIAロールはどうでしょう?
details
に暗黙的に設定されたARIAロールはgroupとなっています。
また、許可されているARIAロールはありません。
group
ロールとは
支援技術によってページの要約や目次に含まれないようにするユーザインタフェースオブジェクトであるとされています。
group
はページ上で主要な知覚可能セクションとするべきではありません。
「例えば、ツリーウィジェットの子ウィジェットが階層構造の兄弟の集まりを形成するように、ウィジェット内のアイテムの論理的な集まりを形成するためにグループを使用すべき」とある通り、details
もここで言うウィジェットに含まれるわけなので、暗黙的にgroupが設定されているようです。
また、もし、そのセクションがWebページの目次に含めるに値するほど重要である場合、そのセクションにregion
や標準的なランドマークロールを割り当てるべきと記載があります。
標準的なランドマークロールは以下の通りありますが、この中でより明確に重要なセクションだと伝えられるロールはregion
です。
ロール名 | 説明 |
---|---|
banner | ページ固有のコンテンツではない、バナー類 |
complementary | ドキュメントの補助的セクション、メインコンテンツから分離しても意味を持つ |
contentinfo | 親文書に関する情報を含む、知覚可能な大きな領域 著作権やプライバシーポリシーへのリンクなど |
form | フォームの役割を持つ要素 |
main | 文書内の主要なコンテンツ |
navigation | ナビゲーション要素(通常はリンク)の集合 |
region | ページの要約にリストアップされるべきコンテンツ ユーザが支援技術などでそのセクションに簡単に移動できる |
search | 検索機能を構成するアイテムやオブジェクト |
region
ロールをつけた時のスクリーンリーダーの挙動
region
ロールは、「目次に羅列されるべき」「ページの要約にリストアップされるべき」コンテンツを明示するとあります。
section
タグなんかはこの目次に羅列されるべきコンテンツに当たるのかと思い、確認してみました。
暗黙のARIAロールは、要素にアクセシブルな名前がある場合はregion
、それ以外の場合は対応するロールなしとあります。
「対応するロールなし」要素は、暗黙のARIAセマンティクスは無いものの、ちゃんと意味を持っており、ARIAが提供しないロール、ステート、プロパティで表され、アクセシビリティAPIを介して支援技術利用のユーザーに知覚される可能性があるものです。
つまり、section
はHTML仕様上で意味を持っているため、わざわざARIAロールを指定する必要がないということです。
そもそもdiv
やspan
のような、セマンティクス的に中立な要素にrole
属性を追加することが推奨されています。
つまり、できるだけHTML標準のタグを使え1、ということです。
そのため、例えば、以下のようなHTMLがあって、本来ARIAのルール上やってはいけませんが、仮にdetails
にregion
ロールをつけて、
<h1>Heading 1</h1>
<section>
<h2>Heading 2</h2>
<p>content text</p>
</section>
<section>
<h2>Heading 2</h2>
<p>content text</p>
</section>
<details role="region" aria-label="title">
<summary>disclosure widget</summary>
<section>
<h2>Heading 2</h2>
<section>
<h3>Heading 3</h3>
<p>content text</p>
</section>
</section>
</details>
macOSのVoiceOverを使うと、ランドマークはこんな感じになります。
ランドマーク
本文
title 地域
section
にランドマークロールがついていないため、details
だけが逆に悪目立ちしてしまいました。
ちなみに、目次は以下のように、折りたたまれている状態だと中のheadingは表示されません。
目次
1. Heading 1
2. Heading 2
2. Heading 2
目次に表示させたいからdetails
にheading
ロール付与させるのはアリ?
ナシよりのナシです。
たしかにheading
ロールを付与すると見出しとして扱われキーボードインタラクションも可能になりますが、前述の通り、details
に許可されているARIAロールはないため、無理やり目次に表示させようとheading
ロールを付与するのは許可されません。
<details role="heading" aria-level="2" aria-label="title">
<summary>disclosure widget</summary>
<section>
<h2>Heading 2</h2>
<section>
<h3>Heading 3</h3>
<p>content text</p>
</section>
</section>
</details>
分かった、じゃあsummary
にheading
ロールを…
ナシよりのナシです。
summary
の暗黙のARIAロールはbutton
、許可されたARIAロールはありませんから、こちらもARIAロールを付与することはできません。
<details>
<summary role="heading" aria-level="2" aria-label="title">disclosure widget</summary>
<section>
<h2>Heading 2</h2>
<section>
<h3>Heading 3</h3>
<p>content text</p>
</section>
</section>
</details>
ARIA Authoring Practices Guide
ARIA的に正しいアコーディオンの設計パターンを見てみると、details
/ summary
を使った実装とは少々異なることが確認できます。
例えば、『各アコーディオンのヘッダボタンは、ページ構造に適したaria-level
の値を設定したheading
ロール を持つ要素にラップされます』とあります。
これは、summary
にはロールを与えられない、という仕様とはまた違った設計です。
しかしながら、あくまで、『これはARIA仕様に準拠した一例であり、セマンティックなHTMLを最大限に活かすことでアクセシビリティの最適化を行うことができる2』と記載がある通り、今はdetails
/ summary
を使った実装を最優先に考えるべきであり、これらの仕様から逸脱する使い方をしなければいけない場合は、一度立ち止まって本当にそのUIを実現するべきなのかを考える必要があるでしょう。
WAI-ARIAまとめ
-
details
、summary
ともにrole
属性を付与することは許可されてない- ARIAでごにゃごにゃすることはできず、素のHTMLで勝負するしかない
-
details
/summary
で実装すると中のコンテンツはスクリーンリーダーの目次上では読み飛ばされるため、ページコンテンツの構成上で重要な項目をdetails
の中に含めることは好ましくない - ARIAで推奨される設計パターンは
details
/summary
で実装したときとは異なるが、セマンティックなHTMLで実装できるならそちらの方が良い
使い方の考察
- ただ長々しいコンテンツを隠すために
details
summary
を用いることは、やってはいけないこと、ではない - アコーディオンUIを使う以上、格納するコンテンツまでもが完全にアクセシブルである必要があるかと言えば、そうではないかも
- 無理にアクセシブルにするためにARIAロールを用いることはご法度
- アコーディオンのアニメーションが必要な場合は、JavaScriptが必要なため、HTMLだけでアコーディオンが実装できるという良さは消える
- ARIAの設計パターンに準拠して、
div
などにARIA属性をつけて実装しても、結局同じかも
- ARIAの設計パターンに準拠して、
- できるだけセマンティックなHTMLで実装するというARIAの基本的な考え方でいけば、
details
で実装するのが最適解- わざわざARIAを使うためにHTMLタグを意味のないものに書き換える程でもない気がする
- 隠したいコンテンツがページ構成上重要なものである場合は、本当にアコーディオンで実装すべきなのかを見直すべき
- もちろんウィジェットを開くためのボタンはアクセシブルである必要があるが、その中身まで考慮するのは難しそう
実装
細かな実装については以下の記事で事細かに記載されています。
問題児くんSafariのバグも考慮されていたり、非常に実践的ですので、ぜひこちらを参照してください。
ちょっとしたアレンジ
上記の例で十分実用的なのですが、Chrome / Edgeだと「検索して、その単語がアコーディオン内にある場合、自動でアコーディオンが開く」というdetails
の良さがpreventDefault()
によって無くなっています。
せっかくなので、こちらも実装しておきます。
閉じるアニメーションの完了時にopen
属性を取っているため、連打しすぎるとアニメーションが正しく表示されませんが、表示はされるので良しとします。それより検索して自動でアコーディオンが開く方を重視します。
/**
* detailsを閉じるアニメーション関数
* @param {HTMLElement} content アニメーションさせるコンテンツ
* @param {HTMLElement} accordion detailsに当たる要素
*/
const closeAnimation = (content, accordion) => {
gsap.to(content, {
height: 0,
duration: 0.3,
ease: 'power3.out',
// 連打対策
overwrite: true,
onComplete: () => {
if (accordion.hasAttribute('open')) {
accordion.removeAttribute('open');
}
},
});
};
/**
* detailsを開くアニメーション関数
* @param {HTMLElement} content アニメーションさせるコンテンツ
* @param {HTMLElement} accordion detailsに当たる要素
*/
const openAnimation = (content, accordion) => {
gsap.fromTo(content,
// fromは初回アニメーションに必要
{
height: 0,
},
{
height: 'auto',
duration: 0.3,
ease: 'power3.out',
// 連打対策
overwrite: true,
onComplete: () => {
if (!accordion.hasAttribute('open')) {
accordion.setAttribute('open', '');
}
},
}
);
};
/**
* MutationObserverを使用して、DOMの変更を検知
* @param {Array} mutationRecords - DOMの変更を表すMutationRecordの配列
*/
const observer = new MutationObserver((mutationRecords) => {
for (const record of mutationRecords) {
const contentElem = record.target.querySelector('.js_content');
if (record.target.hasAttribute('open')) {
record.target.classList.add('is_open');
openAnimation(contentElem, record.target);
}
}
});
const accordionElems = document.querySelectorAll('.js_details');
for (const accordionElem of accordionElems) {
const summaryElem = accordionElem.querySelector('.js_summary');
const contentElem = accordionElem.querySelector('.js_content');
summaryElem.addEventListener('click', e => {
if (accordionElem.classList.contains('is_open')) {
// closeのときはすぐopen属性が消えるとアニメーションが効かないためpreventDefault()
e.preventDefault();
accordionElem.classList.remove('is_open');
closeAnimation(contentElem, accordionElem);
} else {
accordionElem.classList.add('is_open');
openAnimation(contentElem, accordionElem);
}
});
observer.observe(accordionElem, { attributes: true, attributeFilter: ['open'] });
}
まとめ
details
summary
は、アコーディオンUIを実装する際、特に制限なく使えることが分かりました。
また、使う際は、重要なコンテンツは格納しないようにする必要があることが分かりました。
使用する際は、インタラクションなどの仕様を理解した上で、格納するコンテンツは隠してもページ構成上問題ないものなのか判断する必要があります。
問題がある場合は、デザイン意図を汲みつつ、アコーディオン以外のUI実装を考えたりする必要があります。
-
Accordion Example | APG | WAI | W3C Read This Firstより
Robust accessibility can be further optimized by choosing implementation patterns that maximize use of semantic HTML and heeding the warning that No ARIA is better than Bad ARIA. ↩