
どうもこんにちは。
アスカネットのアスカです。
金沢市で、マッサージ師をしたり、個人事業者や団体の発信や運営の支援をAIをフル活用して行っています。
今回は自分がプロディユスしたDuoアーティストのAlter-Iのサイト作成についてです。
※今回の記事のような技術的な記事はバージョンや時期によって改善されている場合があります。
はじめに:アクセシビリティ対応は「妥協の産物」ではない
「アクセシビリティに対応すると、デザインが地味になりますよね?」
Webサイトの制作現場で、こういう質問をよく受けます。
確かに、「アクセシブルにしましょう」という言葉と、「ビジュアルでブランドを表現しましょう」という言葉は、一見すると矛盾するように感じる方が多いと思います。
ところが、両者は両立できます。
しかも、ただ「両立できる」だけではなく、両立して初めて「本物のアクセシブルなサイト」が完成すると、自分は考えています。
今回は、自分が制作・公開した音楽プロジェクト「Alter-I(オルターアイ)」の公式サイトを題材に、
「ビジュアル表現を妥協せず、かつスクリーンリーダーユーザーにとっても快適に使えるサイト」
を実現するために実施した、技術的なポイントを公開します。
特に、Android端末のスクリーンリーダー「TalkBack」で発生する盲点になりがちなバグと、その解決策を中心に、開発者目線で解説します。
案件の前提:当事者であり、開発者でもあるという視点
自分はWebサイトの制作者であると同時に、視覚障害の当事者でもあります。
普段からスクリーンリーダー(VoiceOver、NVDA、TalkBack など)を使ってWebを利用しているので、
「画面を見れば問題なさそう」なサイトでも、
実際に音声で読み上げると致命的な不具合があるケースを、何度も体験してきました。
この当事者目線と開発者目線の両方を持って、Alter-Iのサイトを制作しました。
そして実際、サイトを一度公開した後、自分自身でTalkBack環境でアクセスしてみて、致命的なアクセシビリティのバグを発見しました。
ここから、サイトの本当のアクセシビリティ対応が始まったんです。
発生した問題①:TalkBackがCSSスタイルを実況中継してしまう
サイトを公開後、AndroidのTalkBackでアクセスしたところ、
スクロールアニメーションが設定されているテキストにフォーカスが当たると、テキスト本文ではなく「16ピクセル、色グレー、背景色…」とCSSのスタイル情報を読み上げてしまう
という、想定外の現象が起きていました。
TalkBackのアクセシビリティ設定で詳細な要素を読み上げるという設定があるのですが、
ここをONにしているとこのようなフォントの種類とか大きさとか、色などを読み上げrのですが、
今回はこの設定をオフにしても読み上げたというバグです。
これは、明らかに「読み上げの想定」を超えた挙動です。
ユーザーの立場から見れば、
「テキストを読みたいのに、なぜか書式情報を延々と聞かされる」
という、極めてストレスがかかる体験になります。
原因:transitionとIntersectionObserverの組み合わせがTalkBackを混乱させる
技術的に深掘りすると、原因はこうです。
opacity: 0の要素は視覚的には見えないが、DOMツリーには存在しているため、TalkBackはそこにフォーカスしようとする- フォーカスされた瞬間、ブラウザが要素を画面内に収めるため自動スクロールが発生する
- スクロールにより
IntersectionObserverが発火し、要素に.is-visibleなどのクラスが付与される - クラス付与により
opacity: 0 → 1へのtransitionが始まる - TalkBackのAccessibility APIが、フォーカス中の要素のスタイルが連続的に変化するのを「動的な書式変更」と検知し、現在のスタイル(フォントサイズ、色など)を読み上げてしまう
つまり、視覚効果として一般的な「フェードインアニメーション」が、TalkBack環境ではスタイル情報のダンプを引き起こす、という相性の悪さが原因です。
解決策:opacityのtransitionを外し、transformだけアニメーションさせる
修正前と修正後のCSSを比較します。
/* 修正前:opacityとtransformの両方をアニメーション */
.fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
}
/* 修正後:opacityはtransitionから外し、transformのみアニメーション */
.fade-in {
opacity: 0;
transform: translateY(30px);
transition: transform 0.8s ease-out;
will-change: transform;
}
修正後は、クラスが付与された瞬間に opacity: 1 へ瞬時に切り替わるため、TalkBackから見た「スタイルの連続変化」が起きなくなります。
視覚的には、「下から少し浮き上がる」というアニメーションは残るので、デザイン上のリッチさは犠牲になりません。
ビジュアルを諦めない、でもアクセシブルにするを、この一手で実現できました。
発生した問題②:見出しから段落への移動で「16ピクセル、色黒の開始」と読み上げてしまう
もうひとつ、Alter-Iサイトのヒーローエリア(一番上の大きな見出しエリア)でも、別のバグが発生していました。
「Alter-I」という巨大な見出しから、その下のタグライン(説明文)にスワイプ移動した瞬間、
「16ピクセル、背景色XX、色黒の開始…」と、CSSの書式変更が長々とアナウンスされる
という現象です。
原因:要素間のCSSプロパティの「落差」を検知している
これは、TalkBackの「テキストの書式を読み上げる」設定がオンのユーザー環境で発生します。
具体的な要因は3つ重なっていました。
- 見出しに適用した
text-shadowや、背後の半透明グラデーション層を、Android Accessibility Compositorが「固有の背景色」として誤認識 - スクリーンリーダー専用の隠し文字クラス(
.sr-only)にcolor: #000 !important;などを指定した結果、親要素との間に「色白の終了、色黒の開始」という明確なCSS境界が生まれた 4rem(64px)の見出しから1rem(16px)の段落へフォーカスが移る、極端なフォントサイズの落差
これらが複合して、TalkBackが「明確な書式変更があった」と判定してしまっていました。
解決策:ARIAでノードを結合し、フォーカス移動そのものを発生させない
CSSをいじって解決しようとすると、デザインの自由度が大きく削られてしまいます。
そこで、別のアプローチを取りました。
「フォーカスの移動そのものを起こさない」という発想です。
<!-- 修正前:見出しと段落、それぞれをスワイプで移動する -->
<h1 class="hero__title">Alter-I</h1>
<p class="hero__tagline">碧と紅が交差する場所で、物語が生まれる。</p>
<!-- 修正後:段落をH1の説明文として結合し、段落自体のフォーカスをスキップ -->
<h1 class="hero__title" aria-describedby="hero-tagline-text">Alter-I</h1>
<p id="hero-tagline-text" class="hero__tagline" aria-hidden="true">
碧と紅が交差する場所で、物語が生まれる。
</p>
aria-describedby で見出しと段落を結合し、aria-hidden="true" で段落自体はスクリーンリーダーから「見えない」状態にします。
すると、ユーザーが見出しにフォーカスした瞬間、
「見出し1, Alter-I, 碧と紅が交差する場所で、物語が生まれる。」
と、ひと塊で読み上げられます。
そして右にスワイプしても、段落ノードはスキップされるため、書式変更の比較計算が物理的に発生しません。
これで、過剰な読み上げを完全にシャットアウトしつつ、ビジュアルデザインは一切変更せずに、アクセシブルな実装が実現できました。
発生した問題③:長文モーダルの「閉じる」ボタンが上部にしかない
3つ目の改善ポイントは、CSSのバグというよりUXの問題です。
Alter-Iサイトでは、楽曲の歌詞を表示するためのモーダルダイアログを実装しています。
公開当初、モーダルの上部にしか「×(閉じる)」ボタンを置いていませんでした。
ところが、TalkBackで実際に使ってみると、
- モーダルを開く
- 上から下へスワイプして歌詞を読み進める
- 最後まで読み終わる
- 閉じるためには、また最上部までスワイプして戻らないといけない
という、極めてUXの悪い状態になっていました。
視覚的には、画面右上の「×」ボタンが視界に入っているので問題ありません。
しかし、音声読み上げユーザーにとって、「過去にスワイプ済みの要素」は遠い場所なのです。
解決策:モーダルの末尾にも「閉じる」ボタンを設置する
<div id="lyrics-modal" class="lyrics-modal" role="dialog" aria-modal="true">
<div class="lyrics-modal__content">
<!-- 上部の視覚的な閉じるボタン -->
<button id="modal-close" class="lyrics-modal__close" aria-label="歌詞を閉じる">×</button>
<!-- 歌詞本文 -->
<div id="modal-lyrics" class="lyrics-modal__lyrics">
(歌詞のテキスト...)
</div>
<!-- 下部のアクセシビリティ用の閉じるボタン -->
<div class="lyrics-modal__footer">
<button id="modal-close-bottom" class="lyrics-modal__close-bottom">歌詞を閉じる</button>
</div>
</div>
</div>
JavaScript側では、modalCloseBottom にも同じ closeModal イベントをバインドしました。
これにより、最後まで歌詞を聞き終えた後、1回スワイプするだけで「歌詞を閉じる」ボタンにフォーカスでき、スムーズに元の画面に戻れるようになりました。
本質的な学び:「アクセシビリティの欠如」と「開発者のバグ」を切り分ける
今回の制作と修正を通して得られた、もっとも重要な開発哲学があります。
アクセシビリティの欠如と開発者のバグは、明確に切り分けて考えなければならないということです。
世の中では、両者がひとまとめに「アクセシビリティが悪い」と語られがちです。
ですが、これらは性質がまったく違います。
A. 開発者のミス(バグ)
不適切なタグ選択や、過剰なCSSオーバーライドによって、
「フォーカスが合わない」
「不要な書式情報を読み上げてしまう」
といった現象が起きるケース。
これはHTML/CSS実装の不備であり、セマンティックなマークアップを守っていれば、本来発生しないものです。
B. 真のアクセシビリティ(UI/UXの最適化)
「スワイプの数を最小化する」
「フォーカストラップを設計する」
「閉じた後にフォーカスを元の位置に戻す」など、
ユーザーが迷子にならないための設計上の配慮。
これが、本質的なアクセシビリティです。
「見た目を派手にしたいから、ユーザーが使いづらくなっても仕方ない」では、本末転倒です。
見た目をかっこよくするために当事者が使いづらくなるくらいなら、ビジュアルを妥協してでもシンプルな構造にする
という実用主義こそが、本来Web制作者が持つべきスタンスだと、自分は考えています。
そして、それでもなお、ビジュアルとアクセシビリティを両立させる工夫を諦めない。
これが、Alter-Iサイトで自分が大事にしたことです。
このような対応が必要な案件・お引き受けしている領域
「自社サイトのアクセシビリティ対応をしてほしい」
「すでに公開しているサイトを、アクセシブルなものに改修したい」
「制作会社が作ったサイトで、スクリーンリーダーで使えない箇所がある」
こういったご相談をお受けしています。
特に、スクリーンリーダー実機での動作検証は、
本来であれば、業者が作る全てのサイトやサービスで検証してほしいところです。
実際には、スクリーンリーダーの知識も少ないでしょうから、
当事者ユーザーがいなければ気づけない領域です。
自分はVoiceOver(iPhone)、NVDA(Windows)、TalkBack(Android)の3環境すべてで日常的に検証ができますので、
実機での再現性確認まで含めた、現実的な改修プランをご提案できます。
おわりに
アクセシビリティ対応は、「やったか/やらないか」のチェックリストを埋める作業ではありません。
実機で操作した時の体験そのものを、設計し直す作業です。
そして、その体験は、当事者でなければ気づけないことが、たくさんあります。
「アクセシビリティ対応サイトを作りたい」とお考えの方は、ぜひ一度ご相談ください。
ビジュアルを諦めずに、当事者にも快適に使えるサイトを、一緒に作りましょう。
お問い合わせフォームより、お気軽にどうぞ。
