デジタル庁デザインシステム:アクセシビリティ対応フォームコンポーネントの実装手法
はじめに
Webフォームはユーザーとの主要なインタラクションポイントであり、アクセシビリティ(Accessibility、利用しやすさ)の確保が最も重要な課題の一つです。デジタル庁デザインシステム(DADS)は、日本の行政サービス向けに設計されたデザインシステムで、特にアクセシビリティを最優先事項として位置付けています。
本記事では、DADSのReact版コンポーネントにおけるフォーム要素のアクセシビリティ実装手法を詳細に解説します。WCAG(Web Content Accessibility Guidelines、ウェブコンテンツアクセシビリティガイドライン)準拠の実装パターンから、具体的なコード実装まで、実践的なノウハウを提供します。
アクセシビリティ実装の基本原則
4つの核心原則
HTMLネイティブ機能の活用戦略
DADSでは保守性の観点から、可能な限りHTMLネイティブの機能を使用することを重視しています。これにより、ブラウザのネイティブアクセシビリティ機能を最大限に活用できます。
主要フォームコンポーネントの実装手法
1. 入力フィールド(Input)コンポーネント
import { type ComponentProps, forwardRef } from 'react';
export type InputBlockSize = 'lg' | 'md' | 'sm';
export type InputProps = ComponentProps<'input'> & {
isError?: boolean;
blockSize?: InputBlockSize;
};
export const Input = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const { className, readOnly, isError, blockSize = 'lg', ...rest } = props;
return (
<input
className={`
max-w-full rounded-8 border bg-white px-4 py-3 border-solid-gray-600 text-oln-16N-100 text-solid-gray-800
hover:[&:read-write]:border-black
data-[size=sm]:h-10 data-[size=md]:h-12 data-[size=lg]:h-14
aria-[invalid=true]:border-error-1 aria-[invalid=true]:[&:read-write]:hover:border-red-1000
focus:outline focus:outline-4 focus:outline-black focus:outline-offset-[calc(2/16*1rem)] focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300
read-only:border-dashed
aria-disabled:border-solid-gray-300 aria-disabled:!border-solid aria-disabled:bg-solid-gray-50 aria-disabled:text-solid-gray-420 aria-disabled:pointer-events-none aria-disabled:forced-colors:text-[GrayText] aria-disabled:forced-colors:border-[GrayText]
${className ?? ''}
`}
aria-invalid={isError || undefined}
data-size={blockSize}
readOnly={props['aria-disabled'] ? true : readOnly}
ref={ref}
{...rest}
/>
);
});
実装のポイント
| 機能 | 実装手法 | アクセシビリティ効果 |
|---|---|---|
| エラー状態 | aria-invalid属性 | スクリーンリーダーにエラー状態を通知 |
| フォーカス表示 | 黄色アウトライン | 視覚的に明確なフォーカス表示 |
| 無効状態 | aria-disabled + CSS | キーボード操作の適切な制御 |
| 読み取り専用 | readOnly属性 | 状態の明確な伝達 |
2. チェックボックス(Checkbox)コンポーネント
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => {
const { children, isError, onClick, size = 'sm', ...rest } = props;
const handleDisabled = (e: React.MouseEvent<HTMLInputElement, MouseEvent>) => {
e.preventDefault();
};
const checkbox = (
<span
className={`
flex items-center justify-center shrink-0 rounded-[calc(1/8*100%)]
data-[size=sm]:size-6 data-[size=md]:size-8 data-[size=lg]:size-11
has-[input:hover:not(:focus):not([aria-disabled="true"])]:bg-solid-gray-420
`}
data-size={size}
>
<input
className={`
appearance-none size-3/4 rounded-[calc(2/18*100%)] border-solid-gray-600 bg-white bg-clip-padding
hover:border-black
focus:outline focus:outline-4 focus:outline-black focus:outline-offset-[calc(2/16*1rem)] focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300
checked:border-blue-900 checked:bg-blue-900 checked:hover:border-blue-1100 checked:hover:bg-blue-1100
indeterminate:border-blue-900 indeterminate:bg-blue-900 indeterminate:hover:border-blue-1100 indeterminate:hover:bg-blue-1100
before:hidden before:size-3.5 before:bg-white
checked:before:block checked:before:[clip-path:path('M5.6,11.2L12.65,4.15L11.25,2.75L5.6,8.4L2.75,5.55L1.35,6.95L5.6,11.2Z')]
indeterminate:before:block indeterminate:before:[clip-path:path('M3.25,7.75H10.75V6.25H3.25V7.75Z')]
data-[size=sm]:border-[calc(2/16*1rem)]
data-[size=md]:border-[calc(2/16*1rem)] data-[size=md]:before:origin-top-left data-[size=md]:before:scale-[calc(20/14)]
data-[size=lg]:border-[calc(3/16*1rem)] data-[size=lg]:before:origin-top-left data-[size=lg]:before:scale-[calc(27/14)]
data-[error]:border-error-1 data-[error]:hover:border-red-1000 data-[error]:checked:bg-error-1 data-[error]:checked:hover:bg-red-1000 data-[error]:indeterminate:bg-error-1 data-[error]:indeterminate:hover:bg-red-1000
aria-disabled:!border-solid-gray-300 aria-disabled:!bg-solid-gray-50 aria-disabled:checked:!bg-solid-gray-300 aria-disabled:indeterminate:!bg-solid-gray-300 aria-disabled:before:border-solid-gray-50
forced-colors:!border-[ButtonText] forced-colors:checked:!bg-[Highlight] forced-colors:checked:!border-[Highlight] forced-colors:indeterminate:!bg-[Highlight] forced-colors:indeterminate:!border-[Highlight] forced-colors:before:!bg-[HighlightText] forced-colors:aria-disabled:!border-[GrayText] forced-colors:aria-disabled:checked:!bg-[GrayText]
`}
onClick={props['aria-disabled'] ? handleDisabled : onClick}
ref={ref}
type='checkbox'
data-size={size}
data-error={isError || null}
{...rest}
/>
</span>
);
return children ? (
<label
className='flex w-fit items-start py-2 data-[size=sm]:gap-1 data-[size=md]:gap-2 data-[size=lg]:gap-2'
data-size={size}
>
{checkbox}
<span
className='text-solid-gray-800 data-[size=sm]:pt-px data-[size=sm]:text-dns-16N-130 data-[size=md]:pt-1 data-[size=md]:text-dns-16N-130 data-[size=lg]:pt-2.5 data-[size=lg]:text-dns-17N-130'
data-size={size}
>
{children}
</span>
</label>
) : (
checkbox
);
});
チェックボックスのアクセシビリティ機能
3. セレクトボックス(Select)コンポーネント
export const Select = forwardRef<HTMLSelectElement, SelectProps>((props, ref) => {
const { children, className, isError, blockSize = 'lg', onKeyDown, onMouseDown, ...rest } = props;
const handleDisabledKeyDown = (e: React.KeyboardEvent<HTMLSelectElement>) => {
if (e.code !== 'Tab') {
e.preventDefault();
}
};
const handleDisabledMouseDown = (e: React.MouseEvent<HTMLSelectElement, MouseEvent>) => {
e.preventDefault();
};
return (
<span className='relative'>
<select
className={`
w-full appearance-none border border-solid-gray-600 rounded-8 bg-white pl-4 pr-10 py-[calc(11/16*1rem)] text-oln-16N-100 text-solid-gray-800
hover:border-black
data-[size=sm]:h-10 data-[size=md]:h-12 data-[size=lg]:h-14
aria-[invalid=true]:border-error-1 aria-[invalid=true]:hover:border-red-1000
focus:outline focus:outline-4 focus:outline-black focus:outline-offset-[calc(2/16*1rem)] focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300
aria-disabled:border-solid-gray-300 aria-disabled:bg-solid-gray-50 aria-disabled:text-solid-gray-420 aria-disabled:pointer-events-none aria-disabled:forced-colors:text-[GrayText] aria-disabled:forced-colors:border-[GrayText]
${className ?? ''}
`}
aria-invalid={isError || undefined}
data-size={blockSize}
onMouseDown={props['aria-disabled'] ? handleDisabledMouseDown : onMouseDown}
onKeyDown={props['aria-disabled'] ? handleDisabledKeyDown : onKeyDown}
ref={ref}
{...rest}
>
{children}
</select>
<svg
aria-hidden={true}
className={`
pointer-events-none absolute right-4 top-1/2 -translate-y-1/2
${props['aria-disabled'] ? 'text-solid-gray-420 forced-colors:text-[GrayText]' : 'text-solid-gray-900 forced-colors:text-[CanvasText]'}
`}
fill='none'
height='16'
viewBox='0 0 16 16'
width='16'
>
<path
d='M13.3344 4.40002L8.00104 9.73336L2.66771 4.40002L1.73438 5.33336L8.00104 11.6L14.2677 5.33336L13.3344 4.40002Z'
fill='currentColor'
/>
</svg>
</span>
);
});
キーボードナビゲーションの実装
フォーカス管理のベストプラクティス
| 操作 | 実装方法 | アクセシビリティ効果 |
|---|---|---|
| Tabキー移動 | ネイティブtabindex | 論理的なフォーカス順序 |
| フォーカス表示 | 黄色アウトライン + リング | 視認性の高いフォーカス表示 |
| 無効要素のスキップ | aria-disabled + イベント制御 | 不要な操作の防止 |
| キーボード操作 | 適切なイベントハンドリング | 全ての操作のキーボード対応 |
フォーカスリングの実装例
focus:outline focus:outline-4 focus:outline-black
focus:outline-offset-[calc(2/16*1rem)]
focus:ring-[calc(2/16*1rem)] focus:ring-yellow-300
この実装により、以下の利点が得られます:
- 4pxの黒いアウトラインで明確な視覚的フィードバック
- 2pxのオフセットで要素から離れた表示
- 2pxの黄色リングで強調表示
- 高コントラストによる視認性の向上
エラー処理とユーザー支援
エラー状態の伝達方法
支援テキストの関連付け
// エラーテキストとの関連付け例
<Input
aria-describedby="error-message-id"
aria-invalid={true}
/>
<ErrorText id="error-message-id">
入力内容に誤りがあります
</ErrorText>
高コントラストモード対応
forced-colorsメディアクエリの活用
DADSコンポーネントでは、Windowsの高コントラストモードなどに対応するために、forced-colorsメディアクエリを積極的に活用しています。
aria-disabled:forced-colors:text-[GrayText]
aria-disabled:forced-colors:border-[GrayText]
forced-colors:!border-[ButtonText]
forced-colors:checked:!bg-[Highlight]
対応カラーパターン
| 状態 | 標準モード | 高コントラストモード |
|---|---|---|
| 無効テキスト | text-solid-gray-420 | text-[GrayText] |
| 無効ボーダー | border-solid-gray-300 | border-[GrayText] |
| チェック状態 | bg-blue-900 | bg-[Highlight] |
| ボタンテキスト | text-blue-900 | text-[ButtonText] |
実装チェックリスト
必須アクセシビリティ項目
- 適切なラベル付け:
aria-label,aria-labelledbyの適切な使用 - エラー状態の伝達:
aria-invalidとaria-describedbyの連携 - キーボード操作: 全ての機能のキーボードアクセス可能
- フォーカス表示: 明確で視認性の高いフォーカスインジケーター
- 無効状態の処理:
aria-disabledの適切な使用とイベント制御 - 高コントラスト対応:
forced-colorsメディアクエリの実装 - スクリーンリーダー対応: 適切なARIA属性とセマンティックHTML
推奨アクセシビリティ項目
- 操作可能領域の拡大: タッチターゲットサイズの適切な確保
- 状態変化の通知:
aria-liveによる動的コンテンツの通知 - 複数言語対応: 言語属性の適切な設定
- パフォーマンス最適化: 操作遅延の最小化
まとめ
デジタル庁デザインシステムのフォームコンポーネントは、WCAGガイドラインに完全準拠したアクセシビリティ実装を提供しています。HTMLネイティブ機能を最大限に活用しつつ、必要な箇所ではARIA属性を適切に使用することで、全てのユーザーが利用しやすいフォーム体験を実現しています。
特に重要なのは、単なる技術的な準拠ではなく、実際のユーザー体験を考慮した実装であることです。キーボード操作の快適さ、スクリーンリーダーでの情報伝達の明確さ、視覚的なフィードバックの適切さなど、多角的な観点からアクセシビリティを確保しています。
これらの実装手法は、行政サービスだけでなく、あらゆるWebアプリケーションで応用可能なベストプラクティスです。アクセシビリティを考慮した設計は、結果的に全てのユーザーにとって使いやすいインターフェースを生み出します。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



