株式会社HRBrain
🤔

ヘッドレスCMSのアクセシビリティ問題について考える

に公開

この記事はアドベントカレンダー10日目の記事です。
HRBrainのオウンドメディアサイト「HR大学」は、Next.js + Contentful(ヘッドレスCMS)で運用しています。

https://www.hrbrain.jp/media

オウンドメディアで多くの人に情報を届けるためには、スクリーンリーダー利用者やキーボード操作のみのユーザー、支援技術を使う方などへの配慮が不可欠です。アクセシビリティ対策は、社会的責任であるだけでなく、SEO向上やユーザビリティ改善、幅広い読者層へのリーチというビジネス的利点もあります。しかし、ヘッドレスCMSは従来型のWordPress等と根本的に異なるため、そのまま運用するとアクセシビリティの重大な見落としが起きやすく、注意が必要です。

本記事では、ヘッドレスCMSで起こりやすいアクセシビリティの問題について取り上げていきます。

従来型CMS(WordPress等)との決定的な違い

従来型CMS(WordPress)

編集画面 → プレビュー/テンプレートの確認 → 公開 → ブラウザにHTMLを返す

ヘッドレスCMS

編集画面 → API経由でコンテンツ提供 → フロントエンドでレンダリング(Next.js等) → 公開 → ブラウザにHTMLを返す

ヘッドレスCMSは従来のWordPressなどと異なり、コンテンツと表示が完全に分離されています。この構成では、従来型CMSのように「テーマが適切なら自動的に保たれる」という保証が無い為、アクセシビリティに関しても開発者が意識的に対応する必要があります。

よく起こるアクセシビリティ問題

ヘッドレスCMS + Next.js構成でおきやすい問題をいくつかピックアップしました。

1.クライアントサイドルーティング時のフォーカス管理

// ページ遷移後にフォーカス管理の処理がされていない
const ArticlePage = ({ article }) => {
  return (
    <article>
      <h1>{article.title}</h1>
      <div>{article.body}</div>
    </article>
  );
}

クライアントサイドルーティングを行うと、フォーカスは前のページのリンク上に残ったままになる可能性があります。これにより、スクリーンリーダー利用者やキーボード操作のみのユーザーが、ページ遷移に気づけず混乱することがあります。

解決方法と例

  • ルートが変わるたびに、 <main> やページの最上位の見出しにフォーカスを移します。
  • <main>/見出しに tabIndex={-1} を設定し、ルーティング後に focus() を呼び出します。

https://developer.mozilla.org/ja/docs/Web/API/HTMLElement/focus

const ArticlePage = ({ article }) => {
  const mainRef = React.useRef(null);

  React.useEffect(() => {
    // ページ切替後に main にフォーカス
    mainRef.current?.focus();
  }, [article]);

  return (
    <main ref={mainRef} tabIndex={-1}>
      <h1>{article.title}</h1>
    </main>
  );
};

2.動的コンテンツの通知

// 検索結果が更新されても通知されない
const SearchResults = ({ results, loading }) => {
  if (loading) return <div>読み込み中...</div>;
  
  return (
    <div>
      <div>検索結果{results.length}</div>
      {results.map(article => (
        <ArticleCard key={article.id} article={article} />
      ))}
    </div>
  );
}

上記のコードだと検索結果が更新されても、スクリーンリーダーに通知されません。視覚的には変化が分かりますが、スクリーンリーダーユーザーは結果が変わったことに気づけないという問題が発生します。

解決方法と例

検索結果の件数を表示する要素にaria-live="polite"を設定します。これにより、結果が更新されるとスクリーンリーダーに通知されます。

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Reference/Attributes/aria-live

const SearchResults = ({ results, loading }) => {
  if (loading) return <div>読み込み中...</div>;

  return (
    <div>
      <div aria-live="polite">
        検索結果{results.length}</div>
      {results.map(article => (
        <ArticleCard key={article.id} article={article} />
      ))}
    </div>
  );
};

補足

aria-liveと組み合わせて使用できる属性としてaria-atomicがあります。trueに設定すると、要素内の一部だけでなく全体の内容がスクリーンリーダーに読み上げられます。aria-liveで通知する際に、変更箇所だけでなく全体を伝えたい場合に有効です。更新頻度が高い場合は通知が煩わしくなる可能性があるため、状況に応じて調整が必要です。

https://developer.mozilla.org/ja/docs/Web/Accessibility/ARIA/Attributes/aria-atomic

3.無限スクロール

// 無限スクロールのみ
const ArticleList = () => {
  const { ref, inView } = useInView();
  const [articles, setArticles] = React.useState([]); 

  const loadMore = () => {
    // APIから追加の記事を1件取得してarticlesに追加
    // 実装の詳細は省略
  };

  React.useEffect(() => {
    if (inView) {
      loadMore();
    }
  }, [inView]);

  return (
    <div>
      {articles.map(article => (
        <ArticleCard key={article.id} article={article} />
      ))}
      <div ref={ref} />
    </div>
  );
}

上記の様に無限スクロールのみだと、新しいコンテンツが追加されてもスクリーンリーダーに通知されません。また、特定のページに直接アクセスできず、キーボードユーザーにとって不便です。

解決方法と例

新しい記事が追加されたことをスクリーンリーダーに知らせるため、件数や「新しい記事が追加されました」といった要素に aria-live="polite" を付けます。また、無限スクロールだけでなくページネーションリンクや「もっと見る」ボタンも提供し、キーボードや支援技術ユーザーが任意の位置にアクセスできるようにします。

const ArticleList = () => {
  const { ref, inView } = useInView();
  const [articles, setArticles] = React.useState([]);
  const [newCount, setNewCount] = React.useState(0);

  const loadMore = () => {
    // APIから追加の記事を1件取得してarticlesに追加
    // 実装の詳細は省略
  };

  React.useEffect(() => {
    if (inView) {
      loadMore();
      setNewCount(prev => prev + 1);
    }
  }, [inView]);

  return (
    <div>
      {/* スクリーンリーダーに新着記事を通知 */}
      <div aria-live="polite" aria-atomic="true">
        {newCount > 0 && `${newCount}件の記事が追加されました`}
      </div>

      {articles.map(article => (
        <ArticleCard key={article.id} article={article} />
      ))}

      <div ref={ref} />

      {/* 任意で読み込める「もっと見る」ボタン */}
      <button onClick={loadMore}>もっと見る</button>
    </div>
  );
}

4.埋め込みコンテンツ(YouTube、Twitter等)

const YouTube = ({ id }) => {
  return (
    <iframe
      src={`https://www.youtube.com/embed/${id}`}
      width="560"
      height="315"
    />
  );
}

上記コードはiframeにtitle属性が無いので、スクリーンリーダーは何の埋め込みコンテンツか伝えられません。YouTube動画なのか、Twitter投稿なのか、広告なのか分かりません。

解決方法と例

iframeにtitle属性を設定し、埋め込みコンテンツの内容を説明します。

const YouTube = ({ id, title }) => (
  <iframe
    src={`https://www.youtube.com/embed/${id}`}
    width="560"
    height="315"
    title={title || "YouTube動画"} // タイトルを必ず設定
  />
);

テストで対策

これらの問題を未然に防ぐため、テストを導入するというのも1つの手です。
開発段階ごとのテストで早期発見・防止が可能です。

1.ESLintで開発中の検出

「eslint-plugin-jsx-a11y」でalt属性の抜けやラベルのないフォーム要素、buttonでないdivの使用といった基本的なアクセシビリティ違反を、開発時にすぐ検知できます。

https://github.com/jsx-eslint/eslint-plugin-jsx-a11y

2.コンポーネント単位テスト

「jest-axe」でユニットテスト時に自動検証が出来ます。
コンポーネントが正しくARIA属性を持っているか、フォーカスやラベルの扱いに問題がないかを自動で検出できます。これにより、単体の部品から安全性を担保できます。

https://www.npmjs.com/package/jest-axe

3.ページ全体のE2Eテスト

「@axe-core/playwright」で実際のブラウザ環境をテストできます。
ページ全体や無限スクロール、モーダルなどの動的コンテンツも含めて違反を検出可能です。
https://www.npmjs.com/package/@axe-core/playwright

自動テストでは検出できない問題

自動テストは非常に有効な手段だと考えますが、検出できない問題もあります。
例えば下記のようなものです。

  • altテキストが「画像」ではなく意味のある説明になっているか
  • 「詳細はこちら」が文脈から理解できるか
  • スクリーンリーダーでの読み上げ順序が自然か
  • Tab順序がストレスなく操作できるか

まとめ

Next.js + ヘッドレスCMS構成では、従来型CMSと異なり開発者が意識的にアクセシビリティ対応を行う必要があります。

自動テストは技術的な問題を検出できますが、コンテンツの質や実際の使用感は人間が判断する必要があります。アクセシビリティは一度の対応で完結するものではなく、継続的な改善が必要です。自動テストで検出できる問題は確実に防ぎ、人的チェックが必要な部分は適切なタイミングで実施することで、
より多くのユーザーにとって使いやすいサイトを目指しましょう。

PR

株式会社HRBrainではHRBrainで一緒に働いてくれる仲間を募集しています。
エントリーお待ちしています!

https://www.hrbrain.co.jp/recruit

株式会社HRBrain
株式会社HRBrain

Discussion