LoginSignup
28
14

More than 1 year has passed since last update.

detailsタグでなんでもアコーディオンにしていいのか考える

Last updated at Posted at 2023-02-08

はじめに

最近、アコーディオン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ロールを指定する必要がないということです。

そもそもdivspanのような、セマンティクス的に中立な要素にrole属性を追加することが推奨されています。
つまり、できるだけHTML標準のタグを使え1、ということです。

そのため、例えば、以下のようなHTMLがあって、本来ARIAのルール上やってはいけませんが、仮にdetailsregionロールをつけて、

regionロールを付与したdetails例(やらないでね)
<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
目次に表示させたいからdetailsheadingロール付与させるのはアリ?

ナシよりのナシです。

たしかにheadingロールを付与すると見出しとして扱われキーボードインタラクションも可能になりますが、前述の通り、detailsに許可されているARIAロールはないため、無理やり目次に表示させようとheadingロールを付与するのは許可されません。

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>
分かった、じゃあsummaryheadingロールを…

ナシよりのナシです。

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まとめ

  • detailssummaryともにrole属性を付与することは許可されてない
    • ARIAでごにゃごにゃすることはできず、素のHTMLで勝負するしかない
  • details / summaryで実装すると中のコンテンツはスクリーンリーダーの目次上では読み飛ばされるため、ページコンテンツの構成上で重要な項目をdetailsの中に含めることは好ましくない
  • ARIAで推奨される設計パターンはdetails / summaryで実装したときとは異なるが、セマンティックなHTMLで実装できるならそちらの方が良い

使い方の考察

  • ただ長々しいコンテンツを隠すためにdetails summaryを用いることは、やってはいけないこと、ではない
  • アコーディオンUIを使う以上、格納するコンテンツまでもが完全にアクセシブルである必要があるかと言えば、そうではないかも
    • 無理にアクセシブルにするためにARIAロールを用いることはご法度
  • アコーディオンのアニメーションが必要な場合は、JavaScriptが必要なため、HTMLだけでアコーディオンが実装できるという良さは消える
    • ARIAの設計パターンに準拠して、divなどにARIA属性をつけて実装しても、結局同じかも
  • できるだけセマンティックなHTMLで実装するというARIAの基本的な考え方でいけば、detailsで実装するのが最適解
    • わざわざARIAを使うためにHTMLタグを意味のないものに書き換える程でもない気がする
  • 隠したいコンテンツがページ構成上重要なものである場合は、本当にアコーディオンで実装すべきなのかを見直すべき
    • もちろんウィジェットを開くためのボタンはアクセシブルである必要があるが、その中身まで考慮するのは難しそう

実装

細かな実装については以下の記事で事細かに記載されています。
問題児くんSafariのバグも考慮されていたり、非常に実践的ですので、ぜひこちらを参照してください。

ちょっとしたアレンジ

上記の例で十分実用的なのですが、Chrome / Edgeだと「検索して、その単語がアコーディオン内にある場合、自動でアコーディオンが開く」というdetailsの良さがpreventDefault()によって無くなっています。

せっかくなので、こちらも実装しておきます。

閉じるアニメーションの完了時にopen属性を取っているため、連打しすぎるとアニメーションが正しく表示されませんが、表示はされるので良しとします。それより検索して自動でアコーディオンが開く方を重視します。

JavaScript
/**
 * 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実装を考えたりする必要があります。

  1. Read Me First - No ARIA is better than Bad ARIA

  2. 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.

28
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
14