React Ariaとは?アクセシブルなUIコンポーネントの作り方
React Ariaを使用してアクセシブルなUIコンポーネントを作成する方法を詳しく解説。WAI-ARIAガイドライン準拠、フックベースの実装、実践的なコンポーネント例を紹介します。
React Ariaとは?アクセシブルなUIコンポーネントの作り方
みなさん、Webアプリケーションを作る時に、アクセシビリティのことを考えていますか?
「すべてのユーザーにやさしいUIを作りたい」って思ったことはありませんか? でも、「何から始めればいいんだろう」と悩んでいる方も多いはずです。
今回は、React Ariaを使って、誰でも使いやすいUIコンポーネントを作る方法を説明します。 この記事を読めば、プロレベルのアクセシブルなコンポーネントが作れるようになりますよ!
React Ariaとは
アクセシビリティって何?
React Ariaは、アクセシブルなUIコンポーネントを作るためのライブラリです。
簡単に言うと、障害のある方でも使いやすいWebサイトを作るための道具です。
なぜアクセシビリティが重要なのか
まず、よくある問題のあるボタンを見てみましょう。
// アクセシビリティを考慮していないボタン
const BadButton = ({ onClick, children }) => {
return (
<div onClick={onClick} style={{ cursor: 'pointer' }}>
{children}
</div>
);
};
このボタンには、いくつかの問題があります。 キーボードで操作できないですし、スクリーンリーダーがボタンだと認識してくれません。
でも、React Ariaを使えば、これらの問題を簡単に解決できます。
// React Ariaを使用したアクセシブルなボタン
import { useButton } from '@react-aria/button';
import { useRef } from 'react';
const AccessibleButton = ({ onPress, children, isDisabled }) => {
const ref = useRef();
const { buttonProps } = useButton({ onPress, isDisabled }, ref);
return (
<button
{...buttonProps}
ref={ref}
style={{
backgroundColor: isDisabled ? '#ccc' : '#007bff',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px',
cursor: isDisabled ? 'not-allowed' : 'pointer'
}}
>
{children}
</button>
);
};
このコードを見ると、useButton
フックを使うだけで、アクセシビリティの機能が自動的に追加されます。
キーボード操作やスクリーンリーダー対応が、何も考えなくても動作するんです。
とても便利ですよね!
WAI-ARIAガイドラインって何?
WAI-ARIAは、Webアクセシビリティの国際標準です。
React Ariaが提供する機能の一例をご紹介します。
// React Aria が自動的に提供するアクセシビリティ機能
const AccessibilityFeatures = {
// キーボードナビゲーション
keyboardSupport: {
'Tab': 'フォーカス移動',
'Enter/Space': 'ボタンの実行',
'Arrow Keys': 'リスト内移動',
'Escape': 'ダイアログを閉じる'
},
// ARIAラベルと説明
ariaAttributes: {
'aria-label': 'コンポーネントの名前',
'aria-describedby': '追加の説明',
'aria-expanded': '展開状態',
'aria-selected': '選択状態'
},
// フォーカス管理
focusManagement: {
'focus trapping': 'モーダル内でのフォーカス制御',
'focus restoration': '元の位置への復帰',
'focus visible': '視覚的フォーカス表示'
},
// スクリーンリーダー対応
screenReaderSupport: {
'live regions': '動的コンテンツの通知',
'semantic markup': '意味のあるHTMLタグ',
'role attributes': '要素の役割を明示'
}
};
これらの機能が、React Ariaを使うだけで自動的に組み込まれます。 手動で設定する必要はありません!
React Ariaの特徴
React Ariaの便利な特徴を見てみましょう。
フックベースのアーキテクチャ
React Ariaは、React Hooksを使って作られています。
// React Aria の基本的な使用パターン
import { useButton } from '@react-aria/button';
import { useToggleState } from '@react-stately/toggle';
import { useToggleButton } from '@react-aria/button';
const ToggleButton = ({ children, defaultSelected }) => {
// 状態管理
const state = useToggleState({ defaultSelected });
// アクセシビリティ機能
const ref = useRef();
const { buttonProps } = useToggleButton(
{
isSelected: state.isSelected,
onChange: state.toggle
},
state,
ref
);
return (
<button
{...buttonProps}
ref={ref}
style={{
backgroundColor: state.isSelected ? '#28a745' : '#6c757d',
color: 'white',
border: 'none',
padding: '8px 16px',
borderRadius: '4px'
}}
>
{children}
</button>
);
};
この例では、useToggleState
で状態を管理し、useToggleButton
でアクセシビリティ機能を追加しています。
ロジックとUIが分離されているので、コードが整理しやすいです。
プリミティブコンポーネント
React Ariaには、たくさんの基本的なコンポーネントが用意されています。
// React Aria が提供するコンポーネント
const PrimitiveComponents = {
// 基本的なインタラクション
button: 'useButton',
checkbox: 'useCheckbox',
radio: 'useRadio',
textField: 'useTextField',
// 複雑なコンポーネント
select: 'useSelect',
combobox: 'useComboBox',
listbox: 'useListBox',
menu: 'useMenu',
// レイアウトとナビゲーション
tabs: 'useTabs',
breadcrumbs: 'useBreadcrumbs',
pagination: 'usePagination',
// データ表示
table: 'useTable',
grid: 'useGrid',
tree: 'useTree',
// フィードバック
tooltip: 'useTooltip',
dialog: 'useDialog',
popover: 'usePopover'
};
これらのコンポーネントを組み合わせて、複雑なUIを作ることができます。
他のアクセシビリティライブラリとの比較
React Ariaと他のライブラリの違いを理解してみましょう。
ライブラリの比較表
それぞれのライブラリには、異なる特徴があります。
// React Aria の特徴
const ReactAriaFeatures = {
philosophy: 'ヘッドレス(スタイルなし)',
flexibility: '完全なカスタマイズ可能',
bundle: '必要な部分のみインポート',
standards: 'WAI-ARIA準拠',
hooks: 'React Hooksベース'
};
// 他のライブラリとの比較
const LibraryComparison = {
'React Aria': {
pros: ['完全カスタマイズ', 'WAI-ARIA準拠', '軽量'],
cons: ['スタイリング必要', '学習コスト'],
useCase: 'デザインシステム構築'
},
'Chakra UI': {
pros: ['即座に使用可能', 'テーマシステム'],
cons: ['カスタマイズ制限', 'バンドルサイズ'],
useCase: '迅速なプロトタイピング'
},
'Material-UI': {
pros: ['豊富なコンポーネント', 'デザインシステム'],
cons: ['Material Design固定', '重い'],
useCase: 'Material Design採用'
}
};
用途に応じて、適切なライブラリを選ぶことが大切です。
React Ariaは、自由度の高いデザインを実現したい場合に最適です。
基本的な使用方法
インストールと設定
React Ariaを使い始める準備をしましょう。
パッケージのインストール
まず、必要なパッケージをインストールします。
# React Aria のコアパッケージ
npm install @react-aria/button @react-aria/textfield @react-aria/select
npm install @react-stately/toggle @react-stately/collections
# または必要なパッケージのみ
npm install @react-aria/interactions @react-aria/focus
npm install @react-aria/utils @react-aria/ssr
# 型定義(TypeScript使用時)
npm install @types/react @types/react-dom
React Ariaの良いところは、必要な部分だけをインストールできることです。 プロジェクトのバンドルサイズを小さく保てます。
基本的なプロバイダー設定
SSRアプリケーションでは、プロバイダーの設定が必要です。
// App.js - SSRアプリケーションの場合
import { SSRProvider } from '@react-aria/ssr';
import { I18nProvider } from '@react-aria/i18n';
const App = () => {
return (
<SSRProvider>
<I18nProvider locale="ja-JP">
<MainContent />
</I18nProvider>
</SSRProvider>
);
};
const MainContent = () => {
return (
<div>
<h1>アクセシブルアプリケーション</h1>
<AccessibleForm />
<AccessibleNavigation />
</div>
);
};
export default App;
SSRProvider
は、サーバーサイドレンダリングで必要です。
I18nProvider
は、多言語対応に使用します。
通常のクライアントサイドアプリケーションでは、これらの設定は不要です。
基本的なコンポーネントの作成
React Ariaを使って、基本的なコンポーネントを作ってみましょう。
アクセシブルなボタン
まず、完全にアクセシブルなボタンを作成します。
import { useButton } from '@react-aria/button';
import { useFocusRing } from '@react-aria/focus';
import { mergeProps } from '@react-aria/utils';
import { useRef } from 'react';
const Button = ({
onPress,
children,
isDisabled = false,
variant = 'primary',
size = 'medium'
}) => {
const ref = useRef();
// ボタンの基本機能
const { buttonProps, isPressed } = useButton({
onPress,
isDisabled
}, ref);
// フォーカスリング(キーボードフォーカス時の視覚的表示)
const { focusProps, isFocusVisible } = useFocusRing();
// スタイルの計算
const getButtonStyles = () => {
const baseStyles = {
padding: size === 'small' ? '4px 8px' : size === 'large' ? '12px 24px' : '8px 16px',
fontSize: size === 'small' ? '14px' : size === 'large' ? '18px' : '16px',
border: 'none',
borderRadius: '4px',
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.6 : 1,
transition: 'all 0.2s ease',
outline: isFocusVisible ? '2px solid #007bff' : 'none',
outlineOffset: '2px'
};
const variantStyles = {
primary: {
backgroundColor: isPressed ? '#0056b3' : '#007bff',
color: 'white'
},
secondary: {
backgroundColor: isPressed ? '#5a6268' : '#6c757d',
color: 'white'
},
outline: {
backgroundColor: isPressed ? '#e2e6ea' : 'transparent',
color: '#007bff',
border: '1px solid #007bff'
}
};
return { ...baseStyles, ...variantStyles[variant] };
};
return (
<button
{...mergeProps(buttonProps, focusProps)}
ref={ref}
style={getButtonStyles()}
>
{children}
</button>
);
};
このコードを見ると、useButton
でボタンの基本機能、useFocusRing
でフォーカス表示を実装しています。
mergeProps
を使って、複数のプロパティを安全に結合できます。
使用例も見てみましょう。
// 使用例
const ButtonExample = () => {
return (
<div style={{ display: 'flex', gap: '10px', flexWrap: 'wrap' }}>
<Button onPress={() => alert('Primary clicked!')}>
Primary Button
</Button>
<Button variant="secondary" onPress={() => alert('Secondary clicked!')}>
Secondary Button
</Button>
<Button variant="outline" size="large" onPress={() => alert('Large clicked!')}>
Large Outline Button
</Button>
<Button isDisabled onPress={() => alert('This should not fire')}>
Disabled Button
</Button>
</div>
);
};
様々なスタイルとサイズのボタンを作ることができます。
アクセシブルなテキストフィールド
次に、フォーム入力に必要なテキストフィールドを実装します。
import { useTextField } from '@react-aria/textfield';
import { useFocusRing } from '@react-aria/focus';
import { mergeProps } from '@react-aria/utils';
import { useRef, useState } from 'react';
const TextField = ({
label,
description,
errorMessage,
isRequired = false,
isDisabled = false,
type = 'text',
value,
onChange,
placeholder,
...props
}) => {
const ref = useRef();
const [inputValue, setInputValue] = useState(value || '');
// テキストフィールドの基本機能
const {
labelProps,
inputProps,
descriptionProps,
errorMessageProps
} = useTextField({
label,
description,
errorMessage,
isRequired,
isDisabled,
type,
value: inputValue,
onChange: (value) => {
setInputValue(value);
onChange?.(value);
},
...props
}, ref);
// フォーカスリング
const { focusProps, isFocusVisible } = useFocusRing();
const hasError = !!errorMessage;
const getInputStyles = () => ({
width: '100%',
padding: '8px 12px',
border: `2px solid ${hasError ? '#dc3545' : '#ced4da'}`,
borderRadius: '4px',
fontSize: '16px',
backgroundColor: isDisabled ? '#f8f9fa' : 'white',
outline: isFocusVisible ? '2px solid #007bff' : 'none',
outlineOffset: '2px',
transition: 'border-color 0.2s ease'
});
return (
<div style={{ marginBottom: '16px' }}>
{/* ラベル */}
<label {...labelProps} style={{
display: 'block',
marginBottom: '4px',
fontWeight: '600',
color: hasError ? '#dc3545' : '#333'
}}>
{label}
{isRequired && <span style={{ color: '#dc3545' }}> *</span>}
</label>
{/* 説明文 */}
{description && (
<div
{...descriptionProps}
style={{
fontSize: '14px',
color: '#6c757d',
marginBottom: '4px'
}}
>
{description}
</div>
)}
{/* 入力フィールド */}
<input
{...mergeProps(inputProps, focusProps)}
ref={ref}
placeholder={placeholder}
style={getInputStyles()}
/>
{/* エラーメッセージ */}
{hasError && (
<div
{...errorMessageProps}
style={{
fontSize: '14px',
color: '#dc3545',
marginTop: '4px'
}}
>
{errorMessage}
</div>
)}
</div>
);
};
このテキストフィールドには、ラベル、説明文、エラーメッセージが適切に関連付けられています。
スクリーンリーダーが、これらの情報を正しく読み上げてくれます。
実際の使用例を見てみましょう。
// 使用例
const TextFieldExample = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [errors, setErrors] = useState({});
const validateEmail = (value) => {
if (!value) return 'メールアドレスは必須です';
if (!/\S+@\S+\.\S+/.test(value)) return 'メールアドレスの形式が正しくありません';
return '';
};
const validatePassword = (value) => {
if (!value) return 'パスワードは必須です';
if (value.length < 8) return 'パスワードは8文字以上で入力してください';
return '';
};
const handleEmailChange = (value) => {
setEmail(value);
setErrors(prev => ({ ...prev, email: validateEmail(value) }));
};
const handlePasswordChange = (value) => {
setPassword(value);
setErrors(prev => ({ ...prev, password: validatePassword(value) }));
};
return (
<form style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
<h2>ログインフォーム</h2>
<TextField
label="メールアドレス"
type="email"
isRequired
value={email}
onChange={handleEmailChange}
placeholder="example@email.com"
description="ログインに使用するメールアドレスを入力してください"
errorMessage={errors.email}
/>
<TextField
label="パスワード"
type="password"
isRequired
value={password}
onChange={handlePasswordChange}
placeholder="8文字以上のパスワード"
description="安全なパスワードを設定してください"
errorMessage={errors.password}
/>
<Button
onPress={() => console.log('Login:', { email, password })}
isDisabled={!email || !password || errors.email || errors.password}
style={{ width: '100%', marginTop: '16px' }}
>
ログイン
</Button>
</form>
);
};
バリデーション機能も含めた、完全なログインフォームが作成できました。
エラーメッセージも適切に表示されます。
複雑なコンポーネントの実装
アクセシブルなドロップダウン
もう少し複雑なUIパターンを実装してみましょう。
セレクトコンポーネント
ドロップダウンメニューは、アクセシビリティの実装が特に重要です。
import { useSelect } from '@react-aria/select';
import { useListBox, useOption } from '@react-aria/listbox';
import { useButton } from '@react-aria/button';
import { useFocusRing } from '@react-aria/focus';
import { useSelectState } from '@react-stately/select';
import { mergeProps } from '@react-aria/utils';
import { useRef } from 'react';
const Select = ({
label,
children,
isDisabled,
isRequired,
errorMessage,
description,
...props
}) => {
// 状態管理
const state = useSelectState(props);
// 参照
const ref = useRef();
const {
labelProps,
triggerProps,
valueProps,
menuProps,
descriptionProps,
errorMessageProps
} = useSelect(props, state, ref);
const { focusProps, isFocusVisible } = useFocusRing();
const hasError = !!errorMessage;
const getTriggerStyles = () => ({
width: '100%',
padding: '8px 32px 8px 12px',
border: `2px solid ${hasError ? '#dc3545' : '#ced4da'}`,
borderRadius: '4px',
backgroundColor: isDisabled ? '#f8f9fa' : 'white',
cursor: isDisabled ? 'not-allowed' : 'pointer',
outline: isFocusVisible ? '2px solid #007bff' : 'none',
outlineOffset: '2px',
position: 'relative',
textAlign: 'left'
});
return (
<div style={{ marginBottom: '16px', position: 'relative' }}>
{/* ラベル */}
<label {...labelProps} style={{
display: 'block',
marginBottom: '4px',
fontWeight: '600',
color: hasError ? '#dc3545' : '#333'
}}>
{label}
{isRequired && <span style={{ color: '#dc3545' }}> *</span>}
</label>
{/* 説明文 */}
{description && (
<div {...descriptionProps} style={{
fontSize: '14px',
color: '#6c757d',
marginBottom: '4px'
}}>
{description}
</div>
)}
{/* セレクトトリガー */}
<button
{...mergeProps(triggerProps, focusProps)}
ref={ref}
style={getTriggerStyles()}
>
<span {...valueProps} style={{ display: 'block' }}>
{state.selectedItem ? state.selectedItem.rendered : 'オプションを選択'}
</span>
{/* ドロップダウンアイコン */}
<span style={{
position: 'absolute',
right: '8px',
top: '50%',
transform: `translateY(-50%) rotate(${state.isOpen ? '180deg' : '0deg'})`,
transition: 'transform 0.2s ease'
}}>
▼
</span>
</button>
{/* ドロップダウンメニュー */}
{state.isOpen && (
<ListBox
{...menuProps}
state={state}
style={{
position: 'absolute',
top: '100%',
left: 0,
right: 0,
zIndex: 1000,
marginTop: '4px'
}}
/>
)}
{/* エラーメッセージ */}
{hasError && (
<div {...errorMessageProps} style={{
fontSize: '14px',
color: '#dc3545',
marginTop: '4px'
}}>
{errorMessage}
</div>
)}
</div>
);
};
このセレクトコンポーネントは、キーボードナビゲーションとスクリーンリーダーに完全対応しています。
次に、リストボックスとオプションの実装を見てみましょう。
// リストボックスコンポーネント
const ListBox = ({ state, ...props }) => {
const ref = useRef();
const { listBoxProps } = useListBox(props, state, ref);
return (
<ul
{...listBoxProps}
ref={ref}
style={{
margin: 0,
padding: '4px 0',
listStyle: 'none',
backgroundColor: 'white',
border: '1px solid #ced4da',
borderRadius: '4px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
maxHeight: '200px',
overflow: 'auto'
}}
>
{[...state.collection].map(item => (
<Option key={item.key} item={item} state={state} />
))}
</ul>
);
};
// オプションコンポーネント
const Option = ({ item, state }) => {
const ref = useRef();
const { optionProps, isSelected, isFocused } = useOption(
{ key: item.key },
state,
ref
);
const getOptionStyles = () => ({
padding: '8px 12px',
cursor: 'pointer',
backgroundColor: isSelected ? '#007bff' : isFocused ? '#f8f9fa' : 'transparent',
color: isSelected ? 'white' : '#333',
outline: 'none'
});
return (
<li
{...optionProps}
ref={ref}
style={getOptionStyles()}
>
{item.rendered}
</li>
);
};
// Item コンポーネント(選択肢の定義用)
const Item = ({ children, ...props }) => {
return <>{children}</>;
};
useListBox
とuseOption
を使って、リストの各項目を適切に実装しています。
フォーカス状態と選択状態が視覚的に分かるようになっています。
実際の使用例を見てみましょう。
// 使用例
const SelectExample = () => {
const [selectedCountry, setSelectedCountry] = useState();
const [selectedCategory, setSelectedCategory] = useState();
return (
<div style={{ maxWidth: '400px', margin: '0 auto', padding: '20px' }}>
<h2>アクセシブルなセレクト</h2>
<Select
label="国・地域"
isRequired
selectedKey={selectedCountry}
onSelectionChange={setSelectedCountry}
description="お住まいの国または地域を選択してください"
>
<Item key="jp">日本</Item>
<Item key="us">アメリカ合衆国</Item>
<Item key="uk">イギリス</Item>
<Item key="ca">カナダ</Item>
<Item key="au">オーストラリア</Item>
</Select>
<Select
label="カテゴリ"
selectedKey={selectedCategory}
onSelectionChange={setSelectedCategory}
description="興味のあるカテゴリを選択してください"
>
<Item key="tech">テクノロジー</Item>
<Item key="design">デザイン</Item>
<Item key="business">ビジネス</Item>
<Item key="education">教育</Item>
<Item key="health">健康</Item>
</Select>
<div style={{ marginTop: '20px', padding: '16px', backgroundColor: '#f8f9fa', borderRadius: '4px' }}>
<h3>選択結果</h3>
<p>国: {selectedCountry || '未選択'}</p>
<p>カテゴリ: {selectedCategory || '未選択'}</p>
</div>
</div>
);
};
キーボードの矢印キーで項目を選択でき、Enterキーで確定できます。
とても使いやすいセレクトコンポーネントが完成しました。
アクセシブルなモーダルダイアログ
モーダルダイアログも、アクセシビリティの実装が重要なコンポーネントです。
ダイアログコンポーネント
フォーカス管理とキーボードナビゲーションを備えたモーダルを実装しましょう。
import { useDialog } from '@react-aria/dialog';
import { useModal, useOverlay } from '@react-aria/overlays';
import { useFocusRing } from '@react-aria/focus';
import { mergeProps } from '@react-aria/utils';
import { useRef, useEffect } from 'react';
const Modal = ({
isOpen,
onClose,
children,
title,
size = 'medium',
isDismissable = true
}) => {
const ref = useRef();
const { overlayProps, underlayProps } = useOverlay(
{ isOpen, onClose, isDismissable },
ref
);
const { modalProps } = useModal();
const { dialogProps, titleProps } = useDialog({ title }, ref);
const getSizeStyles = () => {
const sizes = {
small: { width: '400px', maxWidth: '90vw' },
medium: { width: '600px', maxWidth: '90vw' },
large: { width: '800px', maxWidth: '95vw' },
fullscreen: { width: '100vw', height: '100vh' }
};
return sizes[size];
};
// ESCキーでの閉じる処理
useEffect(() => {
const handleEscape = (e) => {
if (e.key === 'Escape' && isDismissable) {
onClose();
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}
}, [isOpen, onClose, isDismissable]);
if (!isOpen) return null;
return (
<div style={{
position: 'fixed',
top: 0,
left: 0,
width: '100%',
height: '100%',
backgroundColor: 'rgba(0, 0, 0, 0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 1000
}} {...underlayProps}>
<div
{...mergeProps(overlayProps, dialogProps, modalProps)}
ref={ref}
style={{
backgroundColor: 'white',
borderRadius: '8px',
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.15)',
outline: 'none',
...getSizeStyles()
}}
>
{/* ヘッダー */}
<div style={{
padding: '20px 24px 16px',
borderBottom: '1px solid #e9ecef',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<h2 {...titleProps} style={{
margin: 0,
fontSize: '20px',
fontWeight: '600'
}}>
{title}
</h2>
{isDismissable && (
<CloseButton onPress={onClose} />
)}
</div>
{/* コンテンツ */}
<div style={{ padding: '20px 24px' }}>
{children}
</div>
</div>
</div>
);
};
このモーダルでは、useOverlay
とuseModal
を使って、適切なフォーカス管理を実装しています。
ESCキーでの閉じる機能も追加されています。
閉じるボタンのコンポーネントも見てみましょう。
// 閉じるボタン
const CloseButton = ({ onPress }) => {
const ref = useRef();
const { buttonProps } = useButton({ onPress, 'aria-label': 'ダイアログを閉じる' }, ref);
const { focusProps, isFocusVisible } = useFocusRing();
return (
<button
{...mergeProps(buttonProps, focusProps)}
ref={ref}
style={{
width: '32px',
height: '32px',
border: 'none',
borderRadius: '4px',
backgroundColor: 'transparent',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
outline: isFocusVisible ? '2px solid #007bff' : 'none',
outlineOffset: '2px'
}}
>
✕
</button>
);
};
aria-label
を使って、スクリーンリーダーユーザーにボタンの目的を伝えています。
確認ダイアログの実装も見てみましょう。
// 確認ダイアログ
const ConfirmDialog = ({
isOpen,
onClose,
onConfirm,
title,
message,
confirmText = '確認',
cancelText = 'キャンセル',
variant = 'default'
}) => {
const handleConfirm = () => {
onConfirm();
onClose();
};
const getVariantStyles = () => {
const variants = {
default: { color: '#007bff' },
danger: { color: '#dc3545' },
warning: { color: '#ffc107' },
success: { color: '#28a745' }
};
return variants[variant];
};
return (
<Modal isOpen={isOpen} onClose={onClose} title={title} size="small">
<div style={{ textAlign: 'center' }}>
<p style={{
marginBottom: '24px',
fontSize: '16px',
lineHeight: '1.5'
}}>
{message}
</p>
<div style={{
display: 'flex',
gap: '12px',
justifyContent: 'center'
}}>
<Button variant="outline" onPress={onClose}>
{cancelText}
</Button>
<Button
onPress={handleConfirm}
style={{
backgroundColor: getVariantStyles().color,
color: 'white'
}}
>
{confirmText}
</Button>
</div>
</div>
</Modal>
);
};
用途に応じて、異なるスタイルの確認ダイアログを作成できます。
実際の使用例を見てみましょう。
// 使用例
const ModalExample = () => {
const [isModalOpen, setIsModalOpen] = useState(false);
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
const [formData, setFormData] = useState({ name: '', email: '' });
const handleSave = () => {
console.log('保存されました:', formData);
setIsModalOpen(false);
};
const handleDelete = () => {
console.log('削除されました');
};
return (
<div style={{ padding: '20px' }}>
<h2>モーダルダイアログ例</h2>
<div style={{ display: 'flex', gap: '12px' }}>
<Button onPress={() => setIsModalOpen(true)}>
フォームを開く
</Button>
<Button
variant="outline"
onPress={() => setIsConfirmOpen(true)}
style={{ color: '#dc3545', borderColor: '#dc3545' }}
>
削除の確認
</Button>
</div>
{/* フォームモーダル */}
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title="ユーザー情報の編集"
size="medium"
>
<div>
<TextField
label="名前"
isRequired
value={formData.name}
onChange={(value) => setFormData(prev => ({ ...prev, name: value }))}
placeholder="お名前を入力"
/>
<TextField
label="メールアドレス"
type="email"
isRequired
value={formData.email}
onChange={(value) => setFormData(prev => ({ ...prev, email: value }))}
placeholder="email@example.com"
/>
<div style={{
display: 'flex',
gap: '12px',
justifyContent: 'flex-end',
marginTop: '24px'
}}>
<Button variant="outline" onPress={() => setIsModalOpen(false)}>
キャンセル
</Button>
<Button onPress={handleSave}>
保存
</Button>
</div>
</div>
</Modal>
{/* 確認ダイアログ */}
<ConfirmDialog
isOpen={isConfirmOpen}
onClose={() => setIsConfirmOpen(false)}
onConfirm={handleDelete}
title="削除の確認"
message="この操作は取り消すことができません。本当に削除しますか?"
confirmText="削除する"
cancelText="キャンセル"
variant="danger"
/>
</div>
);
};
フォーカス管理が完全に機能し、キーボードだけでも操作できるモーダルが完成しました。
とても使いやすいUIになっています。
カスタムフックとユーティリティ
再利用可能なアクセシビリティフック
アクセシビリティ機能を再利用するための、カスタムフックを作成しましょう。
フォーカス管理フック
フォーカス管理を簡単に実装できるフックです。
import { useRef, useEffect } from 'react';
import { useFocusManager } from '@react-aria/focus';
// フォーカス管理のカスタムフック
export const useFocusManagement = (isActive = false) => {
const focusManager = useFocusManager();
const containerRef = useRef();
const previousActiveElement = useRef();
useEffect(() => {
if (isActive && containerRef.current) {
// 現在のフォーカス要素を保存
previousActiveElement.current = document.activeElement;
// コンテナ内の最初のフォーカス可能要素にフォーカス
const firstFocusable = containerRef.current.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) {
firstFocusable.focus();
}
return () => {
// 元の要素にフォーカスを戻す
if (previousActiveElement.current) {
previousActiveElement.current.focus();
}
};
}
}, [isActive]);
const trapFocus = (event) => {
if (!containerRef.current) return;
const focusableElements = containerRef.current.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
if (event.key === 'Tab') {
if (event.shiftKey) {
if (document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
}
} else {
if (document.activeElement === lastElement) {
event.preventDefault();
firstElement.focus();
}
}
}
};
return {
containerRef,
trapFocus,
focusFirst: () => {
const firstFocusable = containerRef.current?.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
firstFocusable?.focus();
},
focusLast: () => {
const focusableElements = containerRef.current?.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const lastElement = focusableElements?.[focusableElements.length - 1];
lastElement?.focus();
}
};
};
このフックを使えば、フォーカストラップを簡単に実装できます。
使用例を見てみましょう。
// 使用例
const FocusTrappedDialog = ({ isOpen, onClose, children }) => {
const { containerRef, trapFocus } = useFocusManagement(isOpen);
if (!isOpen) return null;
return (
<div
ref={containerRef}
onKeyDown={trapFocus}
style={{
position: 'fixed',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
backgroundColor: 'white',
padding: '20px',
border: '1px solid #ccc',
borderRadius: '8px',
zIndex: 1000
}}
>
{children}
<Button onPress={onClose}>閉じる</Button>
</div>
);
};
とても簡単に、フォーカス管理機能を追加できます。
ライブリージョンフック
動的コンテンツの変更をスクリーンリーダーに通知するフックです。
import { useRef, useCallback } from 'react';
// ライブリージョン(動的コンテンツの通知)フック
export const useLiveRegion = (politeness = 'polite') => {
const liveRegionRef = useRef();
const announce = useCallback((message, priority = politeness) => {
if (!liveRegionRef.current) {
// ライブリージョンを動的に作成
const liveRegion = document.createElement('div');
liveRegion.setAttribute('aria-live', priority);
liveRegion.setAttribute('aria-atomic', 'true');
liveRegion.style.position = 'absolute';
liveRegion.style.left = '-10000px';
liveRegion.style.width = '1px';
liveRegion.style.height = '1px';
liveRegion.style.overflow = 'hidden';
document.body.appendChild(liveRegion);
liveRegionRef.current = liveRegion;
}
// メッセージを設定(スクリーンリーダーが読み上げる)
liveRegionRef.current.textContent = message;
// 一定時間後にクリア
setTimeout(() => {
if (liveRegionRef.current) {
liveRegionRef.current.textContent = '';
}
}, 1000);
}, [politeness]);
const announcePolite = useCallback((message) => {
announce(message, 'polite');
}, [announce]);
const announceAssertive = useCallback((message) => {
announce(message, 'assertive');
}, [announce]);
return {
announce,
announcePolite,
announceAssertive
};
};
このフックを使えば、ユーザーに重要な情報を音声で通知できます。
実際の使用例を見てみましょう。
// 使用例:フォーム送信通知
const FormWithNotification = () => {
const { announce } = useLiveRegion();
const [formData, setFormData] = useState({ name: '', email: '' });
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
try {
// フォーム送信処理
await submitForm(formData);
announce('フォームが正常に送信されました');
} catch (error) {
announce('エラーが発生しました。もう一度お試しください');
} finally {
setIsSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit}>
<TextField
label="名前"
value={formData.name}
onChange={(value) => setFormData(prev => ({ ...prev, name: value }))}
/>
<TextField
label="メールアドレス"
type="email"
value={formData.email}
onChange={(value) => setFormData(prev => ({ ...prev, email: value }))}
/>
<Button type="submit" isDisabled={isSubmitting}>
{isSubmitting ? '送信中...' : '送信'}
</Button>
</form>
);
};
フォーム送信の結果を、スクリーンリーダーユーザーに音声で通知できます。
キーボードショートカットフック
キーボードショートカットを簡単に実装できるフックです。
import { useEffect, useCallback } from 'react';
// キーボードショートカット管理フック
export const useKeyboardShortcuts = (shortcuts, isActive = true) => {
const handleKeyDown = useCallback((event) => {
if (!isActive) return;
for (const shortcut of shortcuts) {
const {
key,
ctrl = false,
shift = false,
alt = false,
meta = false,
handler,
preventDefault = true
} = shortcut;
const matchesModifiers =
event.ctrlKey === ctrl &&
event.shiftKey === shift &&
event.altKey === alt &&
event.metaKey === meta;
const matchesKey = event.key.toLowerCase() === key.toLowerCase();
if (matchesKey && matchesModifiers) {
if (preventDefault) {
event.preventDefault();
}
handler(event);
break;
}
}
}, [shortcuts, isActive]);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [handleKeyDown]);
};
このフックを使えば、複雑なキーボードショートカットを簡単に実装できます。
使用例を見てみましょう。
// 使用例:エディターアプリケーション
const TextEditor = () => {
const [content, setContent] = useState('');
const [isBold, setIsBold] = useState(false);
const [isItalic, setIsItalic] = useState(false);
const { announce } = useLiveRegion();
const shortcuts = [
{
key: 's',
ctrl: true,
handler: () => {
saveContent(content);
announce('ドキュメントが保存されました');
}
},
{
key: 'b',
ctrl: true,
handler: () => {
setIsBold(!isBold);
announce(isBold ? '太字を解除しました' : '太字を適用しました');
}
},
{
key: 'i',
ctrl: true,
handler: () => {
setIsItalic(!isItalic);
announce(isItalic ? '斜体を解除しました' : '斜体を適用しました');
}
},
{
key: 'z',
ctrl: true,
handler: () => {
// undo処理
announce('操作を取り消しました');
}
}
];
useKeyboardShortcuts(shortcuts);
return (
<div style={{ padding: '20px' }}>
<div style={{ marginBottom: '20px' }}>
<h2>テキストエディター</h2>
<div style={{ fontSize: '14px', color: '#666' }}>
ショートカット: Ctrl+S (保存), Ctrl+B (太字), Ctrl+I (斜体), Ctrl+Z (取り消し)
</div>
</div>
<div style={{ marginBottom: '16px' }}>
<Button
onPress={() => setIsBold(!isBold)}
variant={isBold ? 'primary' : 'outline'}
style={{ marginRight: '8px' }}
>
<strong>B</strong>
</Button>
<Button
onPress={() => setIsItalic(!isItalic)}
variant={isItalic ? 'primary' : 'outline'}
>
<em>I</em>
</Button>
</div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
style={{
width: '100%',
height: '300px',
padding: '12px',
border: '2px solid #ced4da',
borderRadius: '4px',
fontSize: '16px',
fontWeight: isBold ? 'bold' : 'normal',
fontStyle: isItalic ? 'italic' : 'normal',
resize: 'vertical'
}}
placeholder="ここに文章を入力してください..."
aria-label="文章入力エリア"
/>
</div>
);
};
キーボードショートカットの操作結果を、音声で通知することもできます。
とても使いやすいエディターが完成しました。
実践的なガイドライン
アクセシビリティテスト
作成したコンポーネントのアクセシビリティをテストする方法を見てみましょう。
自動テストツール
React Testing Libraryを使って、アクセシビリティテストを作成できます。
// React Testing Library を使用したアクセシビリティテスト
import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import userEvent from '@testing-library/user-event';
expect.extend(toHaveNoViolations);
describe('Button accessibility', () => {
test('should not have accessibility violations', async () => {
const { container } = render(
<Button onPress={() => {}}>テストボタン</Button>
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
test('should be keyboard accessible', async () => {
const handlePress = jest.fn();
render(<Button onPress={handlePress}>テストボタン</Button>);
const button = screen.getByRole('button', { name: 'テストボタン' });
// フォーカス可能であることを確認
button.focus();
expect(button).toHaveFocus();
// Enterキーで実行できることを確認
await userEvent.keyboard('{Enter}');
expect(handlePress).toHaveBeenCalled();
// Spaceキーで実行できることを確認
await userEvent.keyboard(' ');
expect(handlePress).toHaveBeenCalledTimes(2);
});
test('should have proper ARIA attributes', () => {
render(
<Button
onPress={() => {}}
isDisabled={true}
aria-describedby="help-text"
>
無効なボタン
</Button>
);
const button = screen.getByRole('button');
expect(button).toHaveAttribute('aria-disabled', 'true');
expect(button).toHaveAttribute('aria-describedby', 'help-text');
});
});
これらのテストを実行することで、アクセシビリティの問題を早期に発見できます。
テストフレームワークを使って、継続的に品質を保つことができます。
手動テストのチェックリスト
自動テストだけでなく、手動でのテストも重要です。
// アクセシビリティ手動テストチェックリスト
const AccessibilityChecklist = {
keyboard: {
title: 'キーボードアクセシビリティ',
checks: [
'Tab キーですべての操作可能要素にフォーカスできる',
'Shift+Tab で逆順にフォーカス移動できる',
'Enter/Space キーでボタンを実行できる',
'Arrow キーでリスト項目を移動できる',
'Escape キーでダイアログを閉じることができる',
'フォーカス順序が論理的である',
'フォーカス表示が明確に見える'
]
},
screenReader: {
title: 'スクリーンリーダー対応',
checks: [
'すべての画像に適切な alt テキストがある',
'フォーム要素にラベルが関連付けられている',
'ボタンの目的が明確に説明されている',
'エラーメッセージが適切に通知される',
'ページの構造が見出しで適切に表現されている',
'リストが適切にマークアップされている',
'テーブルにヘッダーが設定されている'
]
},
visual: {
title: '視覚的アクセシビリティ',
checks: [
'色のコントラスト比がWCAG基準を満たしている',
'色以外でも情報を伝えている',
'テキストサイズを200%に拡大しても使用できる',
'アニメーションを無効にできる',
'フォーカス表示が十分に目立つ'
]
},
motor: {
title: '運動機能アクセシビリティ',
checks: [
'クリック領域が十分に大きい(44px以上)',
'ドラッグ操作に代替手段がある',
'タイムアウトを無効にできる',
'誤操作を防ぐ確認機能がある'
]
}
};
これらのチェックリストを使って、体系的にテストすることが重要です。
パフォーマンス最適化
React Ariaを使ったコンポーネントのパフォーマンス最適化方法です。
メモ化とコード分割
重いコンポーネントは、メモ化とコード分割で最適化できます。
import { memo, useMemo, lazy, Suspense } from 'react';
// 重いコンポーネントのメモ化
const ExpensiveAccessibleComponent = memo(({
data,
onItemSelect,
isSelected
}) => {
// 計算量の多い処理をメモ化
const processedData = useMemo(() => {
return data.map(item => ({
...item,
searchableText: `${item.title} ${item.description}`.toLowerCase()
}));
}, [data]);
const { listBoxProps } = useListBox({
items: processedData,
onSelectionChange: onItemSelect
});
return (
<div {...listBoxProps}>
{processedData.map(item => (
<AccessibleListItem
key={item.id}
item={item}
isSelected={isSelected(item.id)}
/>
))}
</div>
);
});
// 遅延ローディング
const LazyComplexComponent = lazy(() => import('./ComplexAccessibleComponent'));
const OptimizedApp = () => {
return (
<div>
{/* 基本コンポーネントは即座に読み込み */}
<AccessibleNavigation />
{/* 複雑なコンポーネントは遅延読み込み */}
<Suspense fallback={<AccessibleLoadingSpinner />}>
<LazyComplexComponent />
</Suspense>
</div>
);
};
適切な最適化により、パフォーマンスを維持しながらアクセシビリティを提供できます。
バンドル最適化のポイントも押さえておきましょう。
// バンドル最適化のためのインポート
const SelectiveImports = () => {
// ❌ 全体をインポート(重い)
// import * as ReactAria from '@react-aria/interactions';
// ✅ 必要な部分のみインポート(軽い)
import { useButton } from '@react-aria/button';
import { useFocusRing } from '@react-aria/focus';
};
必要な部分だけをインポートすることで、バンドルサイズを小さく保てます。
まとめ
React Ariaを使って、アクセシブルなUIコンポーネントを作る方法を説明しました。
React Ariaの魅力
- フックベース: ロジックとUIの分離で、再利用性抜群
- WAI-ARIA準拠: 国際標準に基づく信頼性の高いアクセシビリティ
- ヘッドレス: 完全に自由なデザインカスタマイズ
- 包括的: 簡単なボタンから複雑なダイアログまで対応
実装のコツ
- 段階的導入: 既存プロジェクトでも、部分的に採用できます
- 適切なテスト: 自動テストと手動テストの両方で品質を保つ
- パフォーマンス: メモ化とコード分割で最適化
- チーム共有: アクセシビリティの重要性をチーム全体で理解
アクセシビリティの意味
- 包摂性: すべてのユーザーが使えるWebサイト
- 法的要求: WCAG、ADAなどの基準への準拠
- SEO効果: セマンティックなマークアップによる検索性向上
- 開発効率: 一貫性のあるコンポーネント設計
今後の取り組み
- 早期対応: 設計段階からアクセシビリティを考慮
- 継続的改善: 開発プロセスに組み込んだテスト
- ユーザーの声: 実際の利用者からのフィードバック収集
- 知識向上: チーム全体でのスキルアップ
React Ariaを活用することで、高品質でアクセシブルなWebアプリケーションを作ることができます。
すべてのユーザーにとって使いやすいインターフェースを提供し、より良いWebの実現に貢献しましょう。
アクセシビリティは、現代のWeb開発において必須の要素です。 ぜひ、実際のプロジェクトで試してみてください!
きっと、すべてのユーザーに愛されるWebアプリケーションが作れますよ。