メインコンテンツまでスキップ

タブやアコーディオンの非表示コンテンツにはhidden="until-found"を使うべし

Development

タブやアコーディオンの非表示コンテンツにはdisplay:noneがよく用いられますが、hidden="until-found"を利用するほうがメリットがあります。

hidden=“until-found”で非表示にしたコンテンツはページ内検索でアクセスできる

until-foundhidden属性に新たに追加された属性値です。

参考

hidden - HTML: ハイパーテキストマークアップ言語 | MDN

従来のhidden属性とは違い、until-found"属性値を指定した場合はブラウザのページ内検索やページ内リンクでそのコンテンツが検出された場合、自動でhidden属性が取り除かれて表示することができます。

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

従来のdisplay:noneを使用した非表示ではコンテンツ内にページ内検索でマッチすべきワードがあったとしても検出できませんでしたが、hidden="until-found"を使えばページ内検索でヒットさせることが可能となります。

hidden="until-found"は2024年4月現在ではGoogle ChromeとMicrosoft Edgeのみの対応となっております。SafariやFirefoxではサポートされていませんが、従来のhidden属性として扱われるため、display:noneする実装と動作に変わりはありません。そのため全モダンブラウザの対応を待たずとも今から導入して差し支えないでしょう。

参考

HTML attribute: hidden: until-found value | Can I use... Support tables for HTML5, CSS3, etc

当ブログの目次のアコーディオンやフッターのカテゴリタブはこのhidden="until-found"を使って実装されていますので、Chrome系ブラウザ限定にはなりますが是非ページ内検索を使ってヒットするか試してみてください。

トリガー部分はa要素で実装するのがオススメ

detailsを使わないアコーディオンのボタンや、タブコンテンツのタブ部分を多くの方がbutton要素で実装されているかと思いますが、hidden="until-found"を使用するのならhref属性と非表示コンテンツのidを紐づけてrole属性の値を適切なものに変更したa要素で実装するのをオススメします。

アコーディオンを実装する場合
<a role="button" href="#panel" aria-expanded="true" aria-controls="panel">ボタン</a>
タブを実装する場合
<a role="tab" href="#tabpanel0" id="tab0" aria-selected="true" aria-controls="tabpanel0">タブ</a>

前項で触れましたが、ページ内リンクでそのコンテンツが検出された場合は自動でhidden属性が取り除かれて表示されるというのが理由です。とは言え、JSでデフォルトの動作を無効化して制御するから関係ないじゃんと思われる方もいるとは思います。

しかし、制御しているJSが何らかの事情で動作しなくなったり、そもそもJSが動かない環境でページを閲覧している場合、button要素ではそのコンテンツを開くことができません。一方a要素を利用すれば前述した仕様によりJS無効環境でもコンテンツを開くことが可能になります。

JS無効環境での表示例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

しかし、これだけではhidden="until-found"をサポートしていないSafariやFirefoxではJS無効環境下でコンテンツを開くことができません。そこで、サポートされていない場合は従来のhidden属性として扱われる仕様を利用し、:target擬似クラスを使用することで1行のCSS宣言でJS無効環境のフォールバックが可能となります。(トリガーのデフォルトの動作をpreventDefault()で抑止していることが前提です)

.panel:target {
display: revert;
}
/* 右クリックorタップ長押しで遷移した時にパネルが開いたままなのを嫌うなら */
@media (scripting: none) {
.panel:target {
display: revert;
}
}

原則的にpreventDefault()を行っているのなら:target擬似クラスで指定している宣言は腐るため、トリガーを押下して:target擬似クラスが有効になる=JSが動いていないということになります。ただし、トリガーを右クリックorタップ長押しで別タブで開いた際は:target擬似クラスが有効になりhidden="until-found"が非対応の環境で該当箇所が開きっぱなしになってしまうため、それを嫌う場合はscriptingメディア特性でJSを無効化した場合のみに絞るとよいでしょう。

結果としてJS無効環境を考えるのならばhidden="until-found"のサポート関係なく、アコーディオンのボタンやタブコンテンツのタブ部分はa要素で実装するのが良いでしょう。

SPAのようにJSの利用が前提となっているWebアプリケーションであれば別ですが、一般的なWebサイトであれば実装コストとの兼ね合いにはなりますがJSが無効になった場合でもなるべく多くの情報を得られるように対応しておきたいところです。

また、a要素をそのまま利用すると支援技術は「リンク」と読み上げるので、ボタンであればrole="button"タブであればrole="tab"を指定することも忘れずに。

リセットCSSのhidden属性に対するdisplay:noneには注意

古いリセットCSSやnormalize.cssを利用している場合、次のような指定が含まれているとhidden="until-found"を指定してもページ内検索やページ内リンクでコンテンツを開くことができなくなります。

🙅‍♂ Not Recommended
[hidden] {
display: none !important;
}

もしこのような指定がされている場合は次のCSSに書き換えるか、acab/reset.cssのようなhidden="until-found"に対応した指定がされているリセットCSSを利用すると良さそうです。

🙆‍♂ Recommended
[hidden]:not([hidden='until-found']) {
display: none !important;
}

hidden=“until-found”のデフォルトのUAスタイルシートはcontent-visibility:hiddenなので注意

従来のhidden属性のデフォルトのUAスタイルシートはdisplay:noneですが、hidden="until-found"の場合はcontent-visibility:hiddenで非表示が行われます。

content-visibility:hiddenは内容物に対してはdisplay:noneに近い動きをしますが、指定している要素そのものにはmargin, border, padding, backgroundがレンダリングされます。

参考

content-visibility - CSS: カスケーディングスタイルシート | MDN

現状ではSafari、Firefoxともにcontent-visibilityをサポートしていないこともあり、hidden="until-found"を指定している要素にレンダリングされるスタイルをあてるとブラウザ間で非表示の際のスタイリングに差異が生じてしまいます。

なるべくならhidden="until-found"を指定している要素にそのようなスタイルはあてないほうが良いでしょう。

hidden=“until-found”を利用したアコーディオンの実装例

最後にhidden="until-found"を利用したアコーディオンの実装例を紹介します。当ブログの目次で使われているものと同じものです。

アコーディオンの実装例

See the Pen by tak-dcxi (@tak-dcxi) on CodePen.

アコーディオンの実装には基本的にdetailsを用いますが、以下のHTML構造のようにランドマークの下にトリガーとなる見出しと開閉コンテンツといった構成の場合はdetailsを使わずにhidden="until-found"aria-expanded属性などで実装します。

detailsを使わないマークアップ例
<section aria-labelledby="headingId">
<h2 id="headingId">
<a role="button" href="#panelId" aria-expanded="false" aria-controls="panelId">見出し</a>
</h2>
<div id="panelId" hidden="until-found">コンテンツ</div>
</section>

理由としてはdetailsだとsummary要素の中に見出し要素を含むのはHTMLの仕様違反ではないものの、見出しのroleが消失するアクセシビリティの問題が孕んでいるとのことなのでこちらの構成にしたほうが良さそうです。

role消失の件はMarkuplint開発者の平尾さんから以前教わりました。ありがとうございます。

実装の挙動に関しては以前投稿した記事「アコーディオンのスライドアニメーションはCSS2行で実装できる」で紹介した方法と変わりありません。open属性の処理をaria-expanded属性とhidden属性の処理に変更しているくらいです。

ただし、button要素やsummary要素とは違ってa要素はスペースキーでの操作ができないためkeydownイベントを追加してそれらに合わせています。

const handleClick = (event: Event): void => {
event.preventDefault()
isOpened() ? onClose() : onOpen()
}
// スペースの挙動をbutton要素と合わせる
const handleKeyDown = (event: KeyboardEvent): void => {
if (event.key === ' ') {
handleClick(event)
}
}
button.addEventListener('keydown', handleKeyDown)

また、beforematchイベントリスナーを使用してページ内検索で開いた時にaria-expandedの値を変えるようにしています。

// マッチ前の処理を管理する関数
const handleBeforeMatch = (): void => {
button.setAttribute('aria-expanded', 'true')
}
panel.addEventListener('beforematch', handleBeforeMatch)

detailsの実装でも同様ですが、アニメーションでheightの値を操作する際はCSS側でheight:0を指定するのは避けて、transitionが完了したらheightの値を初期値(auto)に戻すのを忘れないでください。閉じている時にheight:0が指定されているとページ内検索やJS無効環境下でのクリックで要素を開くことができなくなります。

また、閉じた後にdisplay:noneをうっかり指定しないように注意しましょう。

Share

本文上部へ戻る