Hands-on
アプリケーション実装で考慮するポイント
Webサイト、モバイルアプリなどを作成する際に考慮すべき内容をまとめました。
Web Content Accessibility Guidelines (WCAG) 2.2 をベースとしています。
1. 知覚可能 (Perceivable)
コンテンツが、ユーザーが知覚できる方法で提示されているか確認しましょう。
1-1. テキストによる代替
画像や映像など、テキストではないコンテンツには、その内容を説明するテキストによる代替(代替テキスト)を提供することで、スクリーンリーダーや点字ディスプレイなどの支援技術が内容をユーザーに伝えられるようになります。
- 画像の代替テキスト
- 情報を持つ画像: その目的や内容を簡潔に説明する代替テキストを必ず設定しましょう。例えば、ロゴ画像には会社名、グラフ画像にはその内容を説明するテキストが必要です。
- 装飾的な画像: 純粋な装飾目的の画像は、支援技術に無視させるための工夫が必要です(例:
alt="")。 - 参考: 達成基準 1.1.1 非テキストコンテンツ (A)
- 時間依存メディアの代替
- 音声のみのコンテンツ(収録済): 内容を補完する文字起こしや、そのメディアに対する代替コンテンツを提供しましょう。聴覚に障害のある方や、音声を再生できない環境のユーザーが内容を把握できます。
- 映像のみのコンテンツ(収録済): 内容を説明する音声トラック、または代替コンテンツを提供しましょう。
- 同期したメディア(収録済): 会話だけでなく、内容を理解するために必要な効果音や話者の情報などを含むキャプションを提供しましょう。また、視覚的な情報を伝える音声解説も提供しましょう。
- ライブ音声: 生放送などライブ配信される音声には、キャプションを提供しましょう。
- 参考:
1-2. 見やすく、聞きやすいデザイン
ユーザーがコンテンツを判別できるよう、見た目や音に配慮したデザインにしましょう。
- 色の使用
- 色は、情報を伝える唯一の手段にしないでください。色覚特性を持つユーザーのために、色以外(テキスト、アイコン、下線など)の手段も併用しましょう。例えば、必須項目を「赤色」だけで示すのではなく、「(必須)」というテキストを追加します。
- 参考:
- コントラスト比の確保
- テキスト: テキストと背景色には、最低限のコントラスト比を確保しましょう。
- 通常テキスト: 4.5:1 以上。
- サイズの大きなテキスト: 3:1 以上(18pt以上、または14pt以上の太字)。
- 非テキストコンテンツ: アイコン、ボタンの輪郭、グラフの線など、情報を伝えるグラフィカルな要素も、背景とのコントラスト比を3:1以上にしましょう。ただし、コンポーネントが非アクティブな状態や、ユーザーエージェントによってスタイルが決定されている場合は除きます。
- 参考:
- テキスト: テキストと背景色には、最低限のコントラスト比を確保しましょう。
- テキストのリサイズ
- テキストは、コンテンツや機能を損なうことなく、支援技術なしで200%までサイズ変更できるようにしましょう。
- リフロー(Reflow): 画面を400%に拡大しても、縦スクロールのコンテンツが320CSSピクセル、横スクロールのコンテンツが256CSSピクセル内で収まり、二次元スクロール(縦横両方のスクロール)が不要になるようにしましょう。地図やゲームなど、二次元レイアウトが必須なコンテンツは例外です。
- 参考:
- 文字画像の使用を避ける
- テキストは、画像ではなく、CSSなどでスタイル付けされたHTMLテキストとして提供しましょう。フォントやサイズ、色などをユーザーが自由にカスタマイズできるようになります。
- 例外として、ロゴや、特定のフォントで表現することが不可欠な場合は文字画像を使用しても問題ありません。
- 参考:
- ホバーやフォーカスで表示されるコンテンツ
- マウスホバーやキーボードフォーカスでツールチップ、サブメニューなどのコンテンツが表示される場合、以下の要件を満たしましょう。
- 非表示にできる: ホバーやフォーカスを外さずに、閉じるメカニズムを提供しましょう。
- ホバーし続けられる: マウスカーソルを移動させても、追加コンテンツが消えないようにしましょう。
- 表示が継続される: ユーザーが非表示にするまで、コンテンツが表示され続けるようにしましょう。
- 参考:
- マウスホバーやキーボードフォーカスでツールチップ、サブメニューなどのコンテンツが表示される場合、以下の要件を満たしましょう。
2. 操作可能 (Operable)
キーボードや音声、タッチ操作など、様々な方法でコンテンツを操作できるようにしましょう。
- キーボード操作の確保
- すべての機能は、マウスを使わずにキーボードだけで操作できるようにしましょう。
- キーボードトラップの防止: フォーカスが特定のコンポーネント内に閉じ込められ、外に出せなくなる「キーボードトラップ」を発生させてはいけません。
- フォーカスの可視化: キーボードのフォーカスがどこにあるか、常に視覚的にわかるインジケータ(輪郭線など)を提供しましょう。
- 参考:
- 時間制限を設ける場合
- コンテンツに時間制限を設ける場合は、ユーザーがその制限を解除、調整、または延長できるようにしましょう。例えば、フォームの入力時間がタイムアウトする前に警告を出し、時間を延長できるような機能が必要です。
- コンテンツの自動的な動きを止める: 5秒以上動き続けるコンテンツ(カルーセルなど)は、ユーザーが一時停止、停止、または非表示にできるコントロールを提供しましょう。
- 参考:
- ナビゲーションと見出し
- ページタイトル: 各ページのタイトルは、その内容や目的を説明する明確なものにしましょう。
- ブロックスキップ: 複数のページで繰り返されるナビゲーションメニューやヘッダーなど、主要なコンテンツのブロックをスキップできるメカニズムを提供しましょう。
- 見出しとラベル: 見出しはコンテンツの内容を、フォームのラベルはその入力項目の目的を明確に示しましょう。見出しは階層構造を正しく使い、内容を整理しましょう。
- 参考:
- ポインタ操作への配慮
- ドラッグ動作: ドラッグ&ドロップ操作が必要な機能には、ドラッグなしで操作できる代替手段を提供しましょう(例:ボタン操作)。
- ターゲットのサイズ: ボタンやリンクなどのインタラクティブな要素のサイズは、誤操作を防ぐために24 x 24 CSSピクセル以上の大きさを確保しましょう。これにより、タッチ操作のユーザーや手の震えがあるユーザーでも、簡単に操作できるようになります。
- ラベルを含む名前: ユーザーに表示されるテキストラベルは、支援技術に公開される名前(name)に含めましょう。これにより、支援技術のユーザーがラベルと操作対象の関係性を正しく理解できます。
- 参考:
3. 理解可能 (Understandable)
ユーザーがコンテンツやインターフェースの操作方法を理解できるようにしましょう。
- ページの言語
<html>要素にlang属性を付与し、ページの主要な言語を指定しましょう。- コンテンツの一部で言語が変わる場合は、その部分にも
lang属性を設定しましょう。これにより、スクリーンリーダーが適切な発音で読み上げることが可能になります。 - 参考:
- 予測可能な動作
- フォーカス時/入力時: ユーザーが要素にフォーカスしたり、入力内容を変更したりしたときに、予期せぬページの移動やコンテキストの変化(新しいウィンドウの表示など)を自動的に起こさないようにしましょう。
- 一貫したナビゲーション: サイト内の複数のページに共通して表示されるナビゲーションメニューは、常に同じ相対的な順序で配置しましょう。これにより、ユーザーはナビゲーションの構造を予測できます。
- 参考:
- 入力支援
- エラーの特定と提案: フォームでエラーが発生した場合、どの項目がエラーなのか、そしてどのように修正すればよいかを明確なテキストで伝えましょう。
- ラベルまたは説明: 入力フォームの各項目には、ラベルまたは説明文を提供しましょう。プレースホルダーに頼らず、
<label>要素などを適切に利用することが重要です。 - 冗長な入力項目の排除: 同一プロセス内で再入力が必要な情報(例:配送先情報と請求先情報)は、自動入力や選択肢として提供しましょう。ただし、セキュリティのために再入力が必要な場合は例外です。
- 認証のアクセシビリティ: パスワードの記憶や転記を要求するような認知機能テストを認証プロセスに含める場合、パスワードマネージャーによる自動入力など、認知機能に依存しない代替手段を用意しましょう。
- 参考:
4. 堅牢 (Robust)
コンテンツが、様々な環境で安定して動作するようにしましょう。
4-1. HTMLのセマンティクスとWAI-ARIAの活用
HTMLのセマンティクス(意味)を正しく使い、必要に応じてWAI-ARIAを補完することで、支援技術がコンテンツの構造や役割を正しく理解できるようになります。
- セマンティックなHTML構造
- ページの構造を定義する際には、
header、nav、main、footer、article、section、asideなどのランドマーク要素を適切に使用しましょう。これにより、ユーザーはページ内の主要な領域へ効率的に移動できます。 - 見出しには**
h1からh6までの見出しレベル**を正しく使い、階層構造を論理的に保ちましょう。 - フォームやリンク、ボタンなど、ユーザーが操作する要素は、
form、a、buttonといったネイティブなHTML要素を優先的に使用しましょう。 - 参考:
- ページの構造を定義する際には、
- WAI-ARIAの活用
- WAI-ARIAは、セマンティックな意味を持たない要素(
divやspanなど)に、役割(role)、状態(state)、プロパティ(property)などの情報を付与するために使用します。 - 例えば、カスタムコンポーネント(例:タブやアコーディオン)には、
role="tablist"やaria-expanded="false"などの属性を付与することで、支援技術にその役割と状態を伝達できます。 - WAI-ARIAは、ネイティブなHTML要素で解決できない場合にのみ使用し、過剰な使用は避けるようにしましょう。
- 参考:
- WAI-ARIAは、セマンティックな意味を持たない要素(
4-2. ステータスメッセージのアクセシビリティ
- フォームの送信結果、読み込み状況、バリデーションエラーなど、ユーザーに状況を知らせるメッセージは、フォーカスを移動させなくても支援技術が読み取れるように実装しましょう。
aria-live属性などが有効です。 - 参考:
問題がないか確認する
コントラスト
- Contrast
- Figma上でコントラストをチェックできる拡張機能
- WebAIM Contrast Checker
- カラーコードを入力するとWCAG 2.2の達成基準を満たしているかどうかのチェックをおこなえるWebサービス
操作可能かどうか
キーボード操作やスクリーンリーダーなどを使ってコンテンツにアクセス可能になっているか実際に試して確認しましょう。
参考:
HTMLの文章構造
- Markup Validation Service
- HTMLの構文エラーや警告をチェックできるW3Cのツール
- axe DevTools
- HTMLの文章構造だけでなく、WAI-ARIAの利用状況、キーボード操作の可否、コントラストなど、より包括的なチェックをしてくれるツール。Chrome拡張として利用できます。
Linterによる開発段階でのチェック
Linterは、ソースコードの品質を自動でチェックするツールです。アクセシビリティに特化したLinter(eslint-plugin-jsx-a11yやaxe-linterなど)を開発環境に導入することで、alt属性の欠落、不適切なroleの使用、見出しレベルの誤りなど、コーディング中に起こりやすいアクセシビリティの問題をリアルタイムで検知し、修正を促すことができます。
これにより、アクセシビリティの問題が後工程に持ち越されることを防ぎ、開発初期段階から品質を担保することが可能になります。またLinterだけでなく、テストフレームワーク(Playwrightなど)と組み合わせてアクセシビリティの自動テストをCI/CDに組み込むことで、より堅牢なチェック体制を構築し、アクセシビリティの改善を進めやすくなります。
たとえば今閲覧されているこのサイト https://design.pepabo.com/ では、 eslint-plugin-jsx-a11y(eslint-plugin-astro) を導入し、GitHub上でPull Requestを作成するとアクセシビリティに関するチェックが実行されています。
実装例:ヘッドレスUIの活用
ヘッドレスUIとは、UIコンポーネントからスタイル(見た目)を切り離し、機能やロジック、そしてアクセシビリティの挙動のみを提供するライブラリです。従来のUIライブラリがデザインと機能を一体で提供していたのに対し、開発者は見た目を自由にカスタマイズしながら、アクセシビリティが考慮された振る舞い(キーボード操作、WAI-ARIAの正しい利用など)を簡単に実装できます。
たとえば、ドロップダウンメニューやモーダルウィンドウ、アコーディオンといった複雑なUIを自作すると、キーボードでのフォーカス管理や適切なaria-*属性の設定など、アクセシビリティ対応に多くの手間がかかります。Radix UIやReact AriaといったヘッドレスUIライブラリは、これらの複雑なロジックをあらかじめ内包しているため、開発者はデザインに集中しつつ、高品質なアクセシビリティを確保できます。
またそこにスタイリング(CSS)における品質を担保するGMOペパボデザインシステム「Inhouse」を組み合わせることで、アクセシビリティが考慮されたコンポーネントを効率よく実装することができます。
例として、React AriaとInhouseのSassを組み合わせることでアクセシビリティが考慮されたコンポーネントを効率的に実装する方法を示します。
参考:https://github.com/pepabo/inhouse-components-web
実装の基本構造
このプロジェクトでは、以下のような構造でコンポーネントを実装しています:
- TSXファイル(Reactコンポーネント): React Ariaのコンポーネントを使用し、クラス名を動的に生成
- SCSSファイル(スタイル): Inhouseのadapter関数を使用し、クラス名に基づいてスタイルを適用
TSXで生成されたクラス名とSCSSで定義されたクラス名が対応することで、React Ariaの機能とInhouseのデザイントークンが連携します。
実装のポイント
アクセシビリティの確保
- React Ariaが提供するアクセシビリティ機能を活用
- キーボードナビゲーションの自動実装
- 適切なARIA属性の設定
- フォーカス管理の自動化
デザインシステムの一貫性
- Inhouseのadapter関数を使用したデザイントークンの活用
- カラー、スペーシング、タイポグラフィの統一
- レスポンシブデザインの対応
開発効率の向上
- 複雑なアクセシビリティロジックの実装が不要
- デザインに集中できる環境
- 保守性の高いコード構造
1. TextFieldコンポーネント
React AriaのTextField、Input、TextArea、FieldErrorコンポーネントを使用し、Inhouseのadapter関数でスタイリングを実装した例です。
React Ariaの活用
- フォーカス管理
- バリデーション状態の管理
- エラーメッセージの表示
- キーボードナビゲーション
import {
TextField as AriaTextField,
Input as AriaInput,
TextArea as AriaTextArea,
FieldError as AriaFieldError
} from 'react-aria-components';
const TextField: FC<Props> = (props: Props) => {
const {
appearance,
color,
size,
state,
width,
isRequired,
tag = 'input',
value,
...rest
} = props;
// クラス名の生成:SCSSで定義されたクラス名と対応
const wrapperClasses = ['in-textfield'];
const innerClasses = ['_input'];
if (typeof appearance !== 'undefined') {
wrapperClasses.push(`-appearance-${appearance}`); // 例: -appearance-outlined
}
if (typeof color !== 'undefined') {
wrapperClasses.push(`-color-${color}`); // 例: -color-neutral
}
if (typeof size !== 'undefined') {
wrapperClasses.push(`-size-${size}`); // 例: -size-m
}
if (typeof state !== 'undefined') {
innerClasses.push(`--${state}`); // 例: --focused, --disabled
}
if (typeof width !== 'undefined') {
wrapperClasses.push(`-width-${width}`); // 例: -width-full
}
return (
<AriaTextField
value={value?.toString()}
isDisabled={state === 'disabled'}
isRequired={isRequired}
className={wrapperClasses.join(' ')} // "in-textfield -appearance-outlined -size-m"
>
{tag === 'input' ? (
<AriaInput
className={innerClasses.join(' ')} // "_input --focused"
type="text"
{...rest}
/>
) : (
<AriaTextArea
className={innerClasses.join(' ')}
{...rest}
/>
)}
<AriaFieldError className="in-validation-message -color-negative">
{({ validationDetails }) =>
validationDetails.valueMissing
? '必須項目です'
: ''
}
</AriaFieldError>
</AriaTextField>
);
};
Inhouse adapter関数の活用
SCSSファイルでは、TSXで生成されたクラス名(in-textfield、-appearance-*、-size-*など)に対応するスタイルを定義しています。Inhouseのadapter関数を使用してデザイントークンに基づいたスタイリングを実装しています。
@use "@pepabo-inhouse/adapter/functions" as adapter;
// メインのスタイル定義:.in-textfieldクラスに適用
@mixin style($options: variables.$default-option) {
position: relative;
display: inline-block;
box-sizing: border-box;
// TSXで生成されるクラス名._inputに対応
._input {
display: block;
box-sizing: border-box;
width: 100%;
font-family: inherit;
border: none;
outline: 0;
transition: adapter.get-transition-value();
appearance: none;
// TSXで生成されるクラス名--disabledに対応
&.--disabled {
opacity: adapter.get-disabled-surface-opacity();
}
}
// TSXで生成されるクラス名-color-neutral、-color-negativeに対応
@mixin -color-rule($intention: neutral) {
._input {
color: adapter.get-text-color($brightness: light, $name: high_emphasis);
background-color: adapter.get-surface-color($brightness: light, $priority: tertiary);
&::placeholder {
color: adapter.get-text-color($brightness: light, $name: low_emphasis);
}
}
}
// TSXで生成されるクラス名-size-s、-size-m、-size-lに対応
@mixin -size-rule($level: m) {
._input {
padding: #{(adapter.get-interactive-component-height($level: $level) * 0.5) -
(adapter.get-line-height($level: $level, $density: normal) * 0.5)}
adapter.get-spacing-size($level: $level);
font-size: adapter.get-font-size($level: $level);
}
input._input {
height: adapter.get-interactive-component-height($level: $level);
}
}
// TSXで生成されるクラス名-appearance-outlined、-appearance-filledに対応
@mixin -appearance-rule($appearance: outlined, $intention: neutral) {
@if $appearance == outlined {
._input {
background-color: adapter.get-surface-color($brightness: light, $priority: primary);
border-radius: adapter.get-radius-size($level: m);
box-shadow: inset 0 0 0 #{adapter.get-border-size($level: m)}
#{adapter.get-field-border-color($color: $intention, $state: enabled)};
// TSXで生成されるクラス名--focusedに対応
&.--focused {
outline: adapter.get-focus-ring-outline();
outline-offset: adapter.get-focus-ring-outline-offset();
}
}
}
}
}
// エクスポート:.in-textfieldクラスを生成
@mixin export {
.in-textfield {
@include style;
}
}
TSXとSCSSの連携ポイント
- TSXで
className="in-textfield -appearance-outlined -size-m"を生成 - SCSSで
.in-textfield.-appearance-outlined.-size-mのスタイルを定義 - クラス名の命名規則(
-appearance-*、-size-*など)で両者が連携
2. Dialogコンポーネント
React AriaのDialog、DialogTrigger、Modal、ModalOverlayコンポーネントを使用したモーダルダイアログの実装例です。
React Ariaの活用
- モーダルのフォーカストラップ
- キーボードでの閉じる操作(ESCキー)
- 適切なARIA属性の設定
import {
Dialog as AriaDialog,
DialogTrigger as AriaDialogTrigger,
Modal as AriaModal,
ModalOverlay as AriaModalOverlay,
Button,
Heading
} from 'react-aria-components';
const Dialog: FC<Props> = (props: Props) => {
const {
title,
children,
footer,
size = 'm',
alignment = 'center',
buttonFlow = 'row',
triggerLabel,
...rest
} = props;
// クラス名の生成:SCSSで定義されたクラス名と対応
const dialogClassList = ['in-dialog'];
if (size !== 'm') {
dialogClassList.push(`-size-${size}`); // 例: -size-l
}
if (alignment !== 'center') {
dialogClassList.push(`-alignment-${alignment}`); // 例: -alignment-left
}
if (buttonFlow !== 'row') {
dialogClassList.push(`-button-flow-${buttonFlow}`); // 例: -button-flow-column
}
const dialogClasses = dialogClassList.join(' ');
return (
<AriaDialogTrigger>
<Button className="in-button -appearance-outlined">
<span className="_body">{triggerLabel}</span>
</Button>
<AriaModalOverlay className="in-modal">
{/* SCSSの.in-modalクラスに対応 */}
<AriaModal>
<AriaDialog
className={dialogClasses} // "in-dialog -size-l -alignment-left"
{...rest}
>
{({ close }) => (
<>
{/* SCSSの._headerに対応 */}
<div className="_header">
{/* SCSSの._titleに対応 */}
<Heading
id="dialog-title"
className="_title"
>
{title}
</Heading>
</div>
{/* SCSSの._contentに対応 */}
<div className="_content" aria-labelledby="dialog-title">
{children}
</div>
{footer && (
<>
{/* SCSSの._footerに対応 */}
<div
className="_footer"
role="toolbar"
aria-label="ダイアログのアクション"
>
{typeof footer === 'function' ? footer({ close }) : footer}
</div>
</>
)}
</>
)}
</AriaDialog>
</AriaModal>
</AriaModalOverlay>
</AriaDialogTrigger>
);
};
Inhouse adapter関数の活用
モーダルとダイアログのスタイリングにInhouseのadapter関数を活用しています。TSXで使用されるクラス名(in-modal、in-dialog、_header、_title、_content、_footer)に対応するスタイルを定義しています。
@use "@pepabo-inhouse/adapter" as adapter;
// モーダルオーバーレイのスタイル:TSXのclassName="in-modal"に対応
@mixin modal-style() {
position: fixed;
inset: 0;
z-index: adapter.get-major-stack-z-index(6);
display: grid;
place-items: center;
background-color: adapter.get-scrim-background-color();
}
// ダイアログコンテンツのスタイル:TSXのclassName="in-dialog"に対応
@mixin dialog-style($options: variables.$default-options) {
display: flex;
flex-direction: column;
gap: adapter.get-spacing-size(m);
box-sizing: border-box;
max-width: calc(100dvw - adapter.get-spacing-size(l) * 2);
max-height: calc(100dvh - adapter.get-spacing-size(l) * 2);
padding: adapter.get-spacing-size(l);
background: adapter.get-surface-color(light, primary);
border-radius: adapter.get-radius-size(m);
animation: dialog-in 200ms ease-out;
@include adapter.elevation(6);
// TSXで生成されるクラス名-size-s、-size-m、-size-lに対応
@each $variant-size in "s", "m", "l" {
&.-size-#{$variant-size} {
@if $variant-size == "s" {
width: adapter.get-boundary-size(xs);
} @else if $variant-size == "m" {
width: adapter.get-boundary-size(s);
} @else if $variant-size == "l" {
width: adapter.get-boundary-size(m);
}
}
}
// TSXで使用されるクラス名._titleに対応
._title {
@include adapter.text(l);
margin: 0;
}
// TSXで使用されるクラス名._contentに対応
._content {
overflow-y: auto;
&:empty {
display: none;
}
}
// TSXで使用されるクラス名._footerに対応
._footer {
display: flex;
gap: adapter.get-spacing-size(s);
justify-content: center;
}
}
// エクスポート:クラスを生成
@mixin export {
.in-modal {
@include modal-style;
}
.in-dialog {
@include dialog-style;
}
}
TSXとSCSSの連携ポイント
- TSXで
className="in-modal"とclassName="in-dialog -size-l"を生成 - SCSSで
.in-modalと.in-dialog.-size-lのスタイルを定義 - TSXの
className="_header"、"_title"、"_content"、"_footer"がSCSSの子要素セレクタに対応
3. Tabコンポーネント
React AriaのTabs、TabList、Tab、TabPanelコンポーネントを使用したタブコンポーネントの実装例です。
React Ariaの活用
- タブ間のキーボードナビゲーション(矢印キー)
- タブパネルの適切な表示/非表示制御
- ARIA属性の自動管理
import {
Tabs as AriaTabs,
TabList as AriaTabList,
Tab as AriaTab,
TabPanel as AriaTabPanel
} from 'react-aria-components';
// React Ariaの状態に基づいてクラス名を動的に生成
const getTabClassName = ({isSelected}: {isSelected: boolean}) => {
return isSelected ? "_item --activated" : "_item"; // SCSSの.--activatedに対応
};
const getPanelClassName = ({isInert}: {isInert: boolean}) => {
return !isInert ? "_panel --activated" : "_panel"; // SCSSの.--activatedに対応
};
const Tab: FC<Props> = (props: Props) => {
const {
tabItems,
appearance,
align,
density,
size,
isGapless,
ariaLabel,
...rest
} = props;
// クラス名の生成:SCSSで定義されたクラス名と対応
const classList = ['in-tab'];
if (typeof appearance !== 'undefined') {
classList.push(`-appearance-${appearance}`); // 例: -appearance-outlined
}
if (typeof align !== 'undefined') {
classList.push(`-align-${align}`); // 例: -align-start
}
if (typeof density !== 'undefined') {
classList.push(`-density-${density}`); // 例: -density-normal
}
if (typeof size !== 'undefined') {
classList.push(`-size-${size}`); // 例: -size-m
}
if (isGapless) {
classList.push(`-is-gapless`);
}
const classes = classList.join(' ');
return (
<div className={classes} {...rest}>
<AriaTabs>
<AriaTabList className="_list" aria-label={ariaLabel}>
{/* SCSSの._listに対応 */}
{tabItems.map((item) => (
<AriaTab
key={item.id}
id={item.id}
className={getTabClassName} {/* 関数を渡すことで状態に応じたクラス名を生成 */}
>
<span className="_body">{item.label}</span>
{/* SCSSの._bodyに対応 */}
</AriaTab>
))}
</AriaTabList>
{tabItems.map((item) => (
<AriaTabPanel
key={item.id}
id={item.id}
className={getPanelClassName} {/* 関数を渡すことで状態に応じたクラス名を生成 */}
>
{item.content}
</AriaTabPanel>
))}
</AriaTabs>
</div>
);
};
Inhouse adapter関数の活用
タブのスタイリングにInhouseのadapter関数を活用し、一貫したデザインシステムを実現しています。TSXで生成されるクラス名に対応するスタイルを定義しています。
@use "@pepabo-inhouse/adapter" as adapter;
@use "@pepabo-inhouse/cell" as cell;
@mixin style($options: variables.$default-option) {
// TSXで使用されるクラス名._listに対応
._list {
display: flex;
gap: adapter.get-spacing-size(xs);
justify-content: map.get($options, align);
box-shadow: inset 0 calc(adapter.get-border-size($level: m) * -1) 0 0
adapter.get-border-color($brightness: light, $name: high_emphasis);
}
// TSXで生成されるクラス名-align-start、-align-centerに対応
@each $align in variables.$available-align {
&.-align-#{$align} {
._list {
justify-content: $align;
}
}
}
// TSXで使用されるクラス名._itemに対応(getTabClassNameで生成)
._item {
@include cell.style((density: map.get($options, density), size: map.get($options, size)));
top: adapter.get-border-size($level: m);
padding-inline: adapter.get-spacing-size(m);
color: adapter.get-text-color($brightness: light, $name: high_emphasis);
font-weight: bold;
border-radius: adapter.get-radius-size($level: m) adapter.get-radius-size($level: m) 0 0;
outline: none;
// TSXのgetTabClassNameで生成されるクラス名--activatedに対応
&:not(.--activated) {
cursor: pointer;
}
// TSXで使用されるクラス名._bodyに対応
&:focus-visible ._body {
outline: adapter.get-focus-ring-outline();
outline-offset: adapter.get-focus-ring-outline-offset();
}
}
// TSXで使用されるクラス名._panelに対応(getPanelClassNameで生成)
._panel {
width: 100%;
overflow: auto;
&:focus-visible {
outline: adapter.get-focus-ring-outline();
outline-offset: adapter.get-focus-ring-outline-offset();
}
// TSXのgetPanelClassNameで生成されるクラス名--activatedに対応
&:not(.--activated) {
display: none;
}
}
}
// エクスポート:.in-tabクラスを生成
@mixin export {
.in-tab {
@include style;
}
}
TSXとSCSSの連携ポイント
- TSXで
className="in-tab -appearance-outlined -size-m"を生成 - React Ariaの状態(
isSelected、isInert)に基づいて--activatedクラスを動的に追加 - SCSSで
.in-tab.-appearance-outlined.-size-m ._item.--activatedのスタイルを定義 - クラス名の命名規則(
_list、_item、_body、_panel)で両者が連携
4. Breadcrumbコンポーネント
React AriaのBreadcrumbs、Breadcrumb、Linkコンポーネントを使用したパンくずリストの実装例です。
React Ariaの活用
- ナビゲーション構造の適切な表現
- 現在のページの識別
- キーボードナビゲーション
import {
Breadcrumbs as AriaBreadcrumbs,
Breadcrumb as AriaBreadcrumb,
Link as AriaLink
} from 'react-aria-components';
const Breadcrumb: FC<Props> = (props: Props) => {
const {
options,
size,
density,
overflow,
ariaLabel,
...rest
} = props;
// クラス名の生成:SCSSで定義されたクラス名と対応
const classList = ['in-breadcrumb'];
if (typeof size !== 'undefined') {
classList.push(`-size-${size}`); // 例: -size-m
}
if (typeof density !== 'undefined') {
classList.push(`-density-${density}`); // 例: -density-normal
}
if (typeof overflow !== 'undefined') {
classList.push(`-overflow-${overflow}`); // 例: -overflow-scroll
}
const classes = classList.join(' ');
return (
<div className={classes} {...rest}>
{/* SCSSの._listに対応 */}
<AriaBreadcrumbs className="_list" aria-label={ariaLabel}>
{options.map((option) => (
<AriaBreadcrumb key={option.value} className="_item">
{/* SCSSの._itemに対応 */}
{/* React AriaのisCurrent状態に基づいてクラス名を動的に生成 */}
<AriaLink
href={option.value}
className={({ isCurrent }) => isCurrent ? '_current' : '_link'}
>
{option.label}
</AriaLink>
</AriaBreadcrumb>
))}
</AriaBreadcrumbs>
</div>
);
};
Inhouse adapter関数の活用
パンくずリストのスタイリングにInhouseのadapter関数を活用しています。TSXで生成されるクラス名に対応するスタイルを定義しています。
@use "@pepabo-inhouse/adapter" as adapter;
@use "@pepabo-inhouse/cell" as cell;
@use "@pepabo-inhouse/icon" as icon;
@mixin style($options: variables.$default-option) {
@include cell.style((density: map.get($options, density), size: map.get($options, size)));
width: 100%;
padding-inline: adapter.get-spacing-size($level: map.get($options, size));
// TSXで生成されるクラス名-overflow-scrollに対応
&.-overflow-scroll {
overflow-x: auto;
white-space: nowrap;
}
// TSXで使用されるクラス名._listに対応
._list {
display: inline;
flex-wrap: nowrap;
margin: 0;
padding: 0;
list-style: none;
}
// TSXで使用されるクラス名._itemに対応
._item {
display: inline-flex;
align-items: center;
// 区切り文字(Chevronアイコン)を自動挿入
&:not(:last-child)::after {
@include icon.style;
content: "chevron_right";
padding-inline: adapter.get-spacing-size($level: xs);
}
}
// TSXのAriaLinkで生成されるクラス名._linkに対応(isCurrent=falseの場合)
._link {
color: adapter.get-implication-color($name: interactive, $level: 700);
text-decoration: underline;
&:focus-visible {
outline: adapter.get-focus-ring-outline();
}
}
// TSXのAriaLinkで生成されるクラス名._currentに対応(isCurrent=trueの場合)
._current {
color: adapter.get-text-color($brightness: light, $name: high_emphasis);
text-decoration: none;
}
}
// エクスポート:.in-breadcrumbクラスを生成
@mixin export {
.in-breadcrumb {
@include style;
}
}
TSXとSCSSの連携ポイント
- TSXで
className="in-breadcrumb -size-m -overflow-scroll"を生成 - React Ariaの
isCurrent状態に基づいて_linkまたは_currentクラスを動的に生成 - SCSSで
.in-breadcrumb.-size-m.-overflow-scroll ._linkのスタイルを定義 - クラス名の命名規則(
_list、_item、_link、_current)で両者が連携
まとめ
この実装パターンでは、以下のポイントが重要です:
- クラス名の命名規則: TSXとSCSSで一貫した命名規則(
in-*、-appearance-*、-size-*、_*など)を使用 - 状態に基づくクラス名の動的生成: React Ariaの状態(
isSelected、isCurrentなど)に基づいてクラス名を動的に生成 - Inhouse adapter関数の活用: デザイントークンを直接指定せず、adapter関数を通じて一貫したデザインシステムを実現
- アクセシビリティの自動対応: React Ariaが提供する機能により、キーボード操作やARIA属性の管理が自動化される
この組み合わせにより、アクセシビリティが考慮された、保守性の高いコンポーネントを効率的に実装できます。
ユーザーに寄り添うコンテンツ作成ガイドライン(開発者以外の方向け)
このガイドラインは、日々の業務でユーザーとコミュニケーションをとるコンテンツ(スライド、資料、SNS投稿、広告など)を作成する際に、アクセシビリティの視点を取り入れるためのものです。
コンテンツ制作に関わる方はぜひご参照ください。
1. 見た目を意識する:色の使い方と文字の読みやすさ
色だけに頼らない表現を心がける
情報は、色だけで伝えずに、形やアイコン、テキストも併用しましょう。色覚特性を持つユーザーや、白黒印刷された資料を見るユーザーにも内容が伝わります。
- 良い例:
必須項目を赤字と(必須)のテキストで示す。 - 悪い例:
赤字だけで必須項目を示す。 - 参考:
十分なコントラストを確保する
背景と文字の色には、はっきりとした差をつけましょう。光の反射が多い屋外や、視力が弱いユーザーでも内容が読みやすくなります。
- ツールを活用する: コントラスト比を簡単にチェックできるツール(WebAIM Contrast Checkerなど)を使って、色を選びましょう。
- 参考:
2. 情報を正しく伝える:代替テキストと文字起こし
画像や図には説明文を添える
SNSの投稿やスライドの画像などには、その内容を説明するテキストを添えましょう。
- 良い例: 「新商品の発表スライドです。詳細はテキストでもご確認いただけます。」
- SNSの代替テキスト機能: InstagramやX(旧Twitter)には、画像に代替テキストを設定する機能があります。これを使うと、視覚に障害のあるユーザーも画像の内容を理解できます。
- 参考:
動画や音声には文字起こしを添える
動画や音声コンテンツを発信する際は、内容の文字起こしやキャプションを用意しましょう。
- 聴覚に障害のあるユーザー、音を出せない環境のユーザー、集中して聞くのが難しいユーザーなど、より多くの人へ情報が届くようになります。
- 参考:
3. 誰にでも伝わる言葉を選ぶ
専門用語や難しい言葉を避ける
専門用語や業界の略語は、一般のユーザーには伝わりにくい場合があります。できるだけ平易な言葉で説明するか、補足説明を付け加えましょう。
- 良い例:
SEOという言葉を使う場合、「検索エンジン最適化(SEO)」と補足する。 - 参考:
- 達成基準 3.1.3 一般的ではない用語 (AAA) ※AAAレベルの達成基準ですが、より多くの人に伝える上で有効です。
リンクの目的を明確にする
リンクを貼る際は、リンク先の内容がわかるテキストにしましょう。
- 良い例: 「新商品の詳細」
- 悪い例: 「詳しくはこちら」
- 参考: