複数のコンテンツを含むボックス全体まるごとリンク化する時、内部に別のリンクを配置できるような実装方法について考えてみます。
こういったコンテンツをどうやってマークアップするかについてはずっと前から議論されている話題ではあるものの、何かを考慮すると何かを犠牲にしなくてはいけず、完璧な最適解が未だに見つかっていない問題の一つではないでしょうか。
前提として
単純にコンテンツをaタグで囲って問題ない場合はもちろんそれでいいのですが、今回は、そうではなく内部に別のリンクも配置できるようにする必要がある場合の実装についての話になります。
つまり以下のようなコンテンツを考えていきます。
- ボックス全体は
aタグではないが、ボックス全体がリンクのようにふるまう。 - 内部にリンクを配置して、問題なくそのリンクへアクセスできる状態にする。
- アクセシビリティに致命的な問題がない状態で実装する。
投稿記事へのカード型リンクで、カテゴリーやタグなどが記事とは別のリンクになってるようなケースですね。
個人的な必須要件
- ボックス全体のメインリンクも、内部のリンクも、ちゃんとタブ操作できる。
- スクリーンリーダーでちゃんとメインリンクのタイトルが読みあげられる。
- マウスホイールクリックで別窓で開ける。(現在のページに残ったまま)
問題は 3. ですね。これはスクリプトでの再現が難しいです。というか自分にはできませんでした。
WAI ARIAなども意識しながらがんばってリンクの動作を再現していっても、自分はこのマウスホイールクリックの再現ができませんでした。
スクリプトでマウスホイールクリックでリンクを開くことはできますが、新規で開いたページへフォーカス?が移ってしまいます。
デフォルトの挙動は 現在のページに残ったまま別タブで新しくページで開くようになっているのですが、この差は地味に大きいです。
実装方法:メインリンクのクリックエリアをボックス全体へ広げる
個人的な最適解は、やはり普通に擬似要素を使ってリンクのクリックエリアをボックス全体へ広げる手法です。
わざわざスクリプトでdivをリンクとして扱うよりも圧倒的に実装が楽な上に、先ほどの個人的な必須要件も満たしています。
具体的には、自分は以下のような実装をしています。
.linkbox {
position: relative; /* 擬似要素が absolute で配置される際の基準とする */
isolation: isolate; /* 内部のz-index順位と外部のz-indexを混同させない。 */
/* ボックスリンクとしてのスタイル */
display: block;
color: inherit;
text-decoration: none;
pointer-events: auto;
cursor: pointer;
}
/* ボックス全体をクリックした時の対象にしたいメインリンク */
.linkbox__mainlink {
position: static; /* staticにして擬似要素の配置基準を親(.linkbox) までスキップ */
/* テキストリンクのスタイルを打ち消すスタイル */
text-decoration: none;
color: inherit;
}
/* クリックエリア拡大のための擬似要素 */
.linkbox__mainlink::before {
content: '';
position: absolute;
inset: 0;
z-index: 1; /* 他のコンテンツより上に重なるように */
}
/* 内部リンクをクリック可能にする */
:where(.linkbox) a:not(.linkbox__mainlink) {
position: relative; /* z-index を効かせる */
z-index: 2; /* .linkbox__mainlinkの擬似要素より上へ */
@media (any-hover: hover) {
&:hover{
opacity: 0.5;
}
}
}
/* :has がサポートされているブラウザでは、内部のメインリンクにフォーカスがある時はボックス全体に outline を表示する */
@supports selector(:has(*)) {
.linkbox:has(.linkbox__mainlink:focus-visible) {
/* ブラウザ標準スタイルで適用する。(好きにカスタマイズしてください) */
outline: auto 1px;
outline: auto 1px -webkit-focus-ring-color;
}
/* メインリンクのフォーカスは非表示 */
.linkbox__mainlink:focus {
outline: 0;
}
} ※ CSSが書かれていないクラスはLism CSSのものです。
また、Tab キーでフォーカスした時にアウトラインをボックス全体に表示することもできたりします。
この実装の問題点
基本的にクリティカルな問題はなく、一般的にも最適解として認識されているかと思いますが、実は一点だけ問題があります。
この実装方法だと、擬似要素が邪魔をしてaltキーを押したままのテキスト選択ができません。
そもそも、通常のテキストリンクがaltキーを押してる間はテキスト選択できるようになっている、ということを知らない人も多いと思いますが、そのような仕様が存在しています。
以下に普通のテストリンクを置いておくので、alt 押しながらテキスト選択してみてください。
ですが、このページで紹介したリンクボックスの実装方法では、ボックス全体に重ねている擬似要素が邪魔をするせいで、altキーを押していてもテキスト選択ができません。
ただ、この問題に関しては無視できる範囲かなとは思います。
理由は以下の通りです。
- そもそも問題となる alt キー押しながらのテクニック知ってる人が少なそう。
- 前後のコンテンツを含めてマウスドラッグで選択すれば、リンク全体の文字列をコピーすることはできる。
実際、大手サイトや大手サービスでも擬似要素を使った実装がよく見られますが、このaltキーを押しながらのテキスト選択ができるように考慮されているサイトは(私はまだ)見たことがありません。
altキー押下中にテキストを選択できるようにするスクリプト
無視でいいとは言ったものの、問題が残っているのは気持ち悪いです。
ですので、スクリプトで解決する方法を模索してみました。
私が思いついたアプローチは次のようなものです。
- altキーの押下を検知し、linkboxへそれを伝えるクラスを付与する。
- そのクラスがついている間、擬似要素を非表示にしてコンテンツを選択できるようにする。
実際の動作は以下のデモをご覧ください。
別タブで表示して確認してみてください。下に表示しているデモはiframe埋め込みのため、少しだけ動作が不安定です。(初回のテキスト選択がうまくいかない可能性が高いです。)
/* linkboxスタイル */
@import '../_src.css';
/* ._linkoff クラスが付与されている間は、擬似要素を非表示にする */
.linkbox._linkoff .linkbox__mainlink::before {
content: none; /* pointer-events: none; でも OK */
}
/* 状態がわかりやすいように色付け(デモ用) */
.linkbox._linkoff {
color: green;
border-color: green;
} ※ CSSが書かれていないクラスはLism CSSのものです。
どうでしょう?いい感じに動作しているように感じます。(他にやってる例が見つからないので、AIと一緒に実装したものになります。)
もしこのスクリプトに何か問題点があれば、ぜひ教えていただけたら助かります。
まとめ
ボックス全体をリンク化する実装では、スクリプトでクリック挙動を再現しようとするアプローチも見かけますが、マウスホイールクリックの挙動など完全な再現は難しいです。
擬似要素を使ってメインリンクのクリックエリアを広げる手法であれば、ネイティブなリンクの挙動をほぼそのまま活かせるため、そういった問題を回避でき、実装もかなり簡単です。
唯一の懸念点としてaltキー押下時のテキスト選択に関する問題がありますが、スクリプトを導入すれば対応できました。