Tailwind CSS で通知・トースト UI を実装するコンポーネント設計術

Web アプリケーションにおいて、ユーザーに適切なフィードバックを提供することは、優れたユーザー体験を実現する上で欠かせない要素です。特に通知やトースト UI は、操作の成功・失敗、重要な情報の提示、システムの状態変化を伝える重要な役割を果たします。
しかし、多くの開発者が直面する課題があります。それは「美しく、使いやすく、保守性の高い通知システムをどのように設計するか」ということです。Tailwind CSS を使えば、この課題を効率的に解決できることを、この記事でお伝えします。
実際のプロジェクトで使える実装例と、よくあるエラーとその解決策を交えながら、段階的に学んでいきましょう。あなたのアプリケーションが、ユーザーにとってより親しみやすく、信頼できるものになることを願っています。
通知・トースト UI の基本概念
通知 UI の種類と特徴
通知 UI には主に以下の種類があります:
種類 | 特徴 | 使用場面 |
---|---|---|
トースト通知 | 画面の一角に一時的に表示 | 操作完了の確認 |
インライン通知 | コンテンツ内に直接表示 | フォームエラー、警告 |
バナー通知 | 画面の上部または下部に固定 | 重要なシステムメッセージ |
モーダル通知 | 画面を覆う形で表示 | 重要な確認や警告 |
通知の基本要素
効果的な通知 UI には以下の要素が含まれます:
- 視覚的な階層: 重要度に応じた色分け
- 明確なメッセージ: 簡潔で理解しやすい文言
- 適切なタイミング: ユーザーの操作に応じた表示
- 自動消去機能: 一定時間後の自動非表示
- 手動制御: ユーザーによる閉じる機能
よくある設計ミスとその影響
多くの開発者が陥りがちな問題があります:
typescript// ❌ 悪い例:色だけで情報を伝えようとする
<div className="bg-red-500 text-white p-4">
エラーが発生しました
</div>
// ✅ 良い例:アイコンとテキストで明確に伝える
<div className="bg-red-500 text-white p-4 flex items-center">
<svg className="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
</svg>
エラーが発生しました。もう一度お試しください。
</div>
Tailwind CSS でのスタイリング基礎
Tailwind CSS の設定と準備
まず、Tailwind CSS をプロジェクトに導入しましょう。
bash# Yarn を使用して Tailwind CSS をインストール
yarn add -D tailwindcss postcss autoprefixer
yarn tailwindcss init -p
tailwind.config.js の基本設定
javascript/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}',
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
// 通知用のカスタムカラーを定義
colors: {
notification: {
success: '#10B981',
warning: '#F59E0B',
error: '#EF4444',
info: '#3B82F6',
},
},
// アニメーション用のキーフレーム
keyframes: {
slideIn: {
'0%': {
transform: 'translateX(100%)',
opacity: '0',
},
'100%': {
transform: 'translateX(0)',
opacity: '1',
},
},
slideOut: {
'0%': {
transform: 'translateX(0)',
opacity: '1',
},
'100%': {
transform: 'translateX(100%)',
opacity: '0',
},
},
},
animation: {
'slide-in': 'slideIn 0.3s ease-out',
'slide-out': 'slideOut 0.3s ease-in',
},
},
},
plugins: [],
};
よくある設定エラーと解決策
javascript// ❌ エラー:content パスが正しく設定されていない
module.exports = {
content: [
'./src/**/*.{js,ts,jsx,tsx}', // このパスが存在しない場合
],
// ...
};
// エラーメッセージ:
// "warn - No utility classes were detected in your source files. If this is unexpected, double-check the `content` option in your Tailwind CSS configuration."
// ✅ 解決策:正しいパスを設定
module.exports = {
content: [
'./app/**/*.{js,ts,jsx,tsx}',
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
// ...
};
通知用の基本スタイルクラス
css/* globals.css または styles.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* 通知用のカスタムコンポーネントクラス */
@layer components {
.notification-base {
@apply fixed z-50 p-4 rounded-lg shadow-lg border-l-4;
}
.notification-success {
@apply bg-green-50 border-green-500 text-green-800;
}
.notification-error {
@apply bg-red-50 border-red-500 text-red-800;
}
.notification-warning {
@apply bg-yellow-50 border-yellow-500 text-yellow-800;
}
.notification-info {
@apply bg-blue-50 border-blue-500 text-blue-800;
}
}
シンプルな通知コンポーネントの実装
基本的な通知コンポーネント
まず、最もシンプルな通知コンポーネントから始めましょう。
typescript// components/Notification.tsx
import React from 'react';
interface NotificationProps {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
isVisible: boolean;
onClose: () => void;
}
const Notification: React.FC<NotificationProps> = ({
type,
message,
isVisible,
onClose,
}) => {
if (!isVisible) return null;
const getTypeStyles = () => {
switch (type) {
case 'success':
return 'bg-green-50 border-green-500 text-green-800';
case 'error':
return 'bg-red-50 border-red-500 text-red-800';
case 'warning':
return 'bg-yellow-50 border-yellow-500 text-yellow-800';
case 'info':
return 'bg-blue-50 border-blue-500 text-blue-800';
default:
return 'bg-gray-50 border-gray-500 text-gray-800';
}
};
return (
<div
className={`fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg border-l-4 ${getTypeStyles()}`}
>
<div className='flex items-center justify-between'>
<span className='text-sm font-medium'>
{message}
</span>
<button
onClick={onClose}
className='ml-4 text-gray-400 hover:text-gray-600'
>
<svg
className='w-4 h-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
</div>
);
};
export default Notification;
使用例とエラーハンドリング
typescript// pages/index.tsx または App.tsx
import React, { useState } from 'react';
import Notification from '../components/Notification';
const App: React.FC = () => {
const [notification, setNotification] = useState({
isVisible: false,
type: 'success' as const,
message: '',
});
const showNotification = (
type: 'success' | 'error' | 'warning' | 'info',
message: string
) => {
setNotification({
isVisible: true,
type,
message,
});
};
const handleClose = () => {
setNotification((prev) => ({
...prev,
isVisible: false,
}));
};
return (
<div className='min-h-screen bg-gray-100 p-8'>
<div className='max-w-md mx-auto space-y-4'>
<button
onClick={() =>
showNotification(
'success',
'操作が完了しました!'
)
}
className='w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600'
>
成功通知を表示
</button>
<button
onClick={() =>
showNotification(
'error',
'エラーが発生しました'
)
}
className='w-full bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600'
>
エラー通知を表示
</button>
</div>
<Notification
type={notification.type}
message={notification.message}
isVisible={notification.isVisible}
onClose={handleClose}
/>
</div>
);
};
export default App;
よくある実装エラーと解決策
typescript// ❌ エラー:TypeScript の型エラー
interface NotificationProps {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
isVisible: boolean;
onClose: () => void;
}
const Notification: React.FC<NotificationProps> = ({
type,
message,
isVisible,
onClose,
}) => {
// エラー:Type 'string' is not assignable to type 'success' | 'error' | 'warning' | 'info'
const getTypeStyles = (type: string) => {
// ...
};
};
// ✅ 解決策:型を正しく指定
const getTypeStyles = (
type: 'success' | 'error' | 'warning' | 'info'
) => {
// ...
};
アニメーション付きトーストの作成
アニメーション機能の実装
次に、スムーズなアニメーションを追加して、より洗練されたトースト通知を作成しましょう。
typescript// components/AnimatedToast.tsx
import React, { useEffect, useState } from 'react';
interface ToastProps {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
duration?: number;
isVisible: boolean;
onClose: () => void;
}
const AnimatedToast: React.FC<ToastProps> = ({
type,
message,
duration = 5000,
isVisible,
onClose,
}) => {
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
if (isVisible) {
setIsAnimating(true);
// 自動消去のタイマー
const timer = setTimeout(() => {
handleClose();
}, duration);
return () => clearTimeout(timer);
}
}, [isVisible, duration]);
const handleClose = () => {
setIsAnimating(false);
setTimeout(() => {
onClose();
}, 300); // アニメーション完了後に非表示
};
const getTypeStyles = () => {
switch (type) {
case 'success':
return 'bg-green-500 text-white';
case 'error':
return 'bg-red-500 text-white';
case 'warning':
return 'bg-yellow-500 text-white';
case 'info':
return 'bg-blue-500 text-white';
default:
return 'bg-gray-500 text-white';
}
};
const getIcon = () => {
switch (type) {
case 'success':
return (
<svg
className='w-5 h-5'
fill='currentColor'
viewBox='0 0 20 20'
>
<path
fillRule='evenodd'
d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z'
clipRule='evenodd'
/>
</svg>
);
case 'error':
return (
<svg
className='w-5 h-5'
fill='currentColor'
viewBox='0 0 20 20'
>
<path
fillRule='evenodd'
d='M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z'
clipRule='evenodd'
/>
</svg>
);
default:
return null;
}
};
if (!isVisible) return null;
return (
<div className='fixed top-4 right-4 z-50'>
<div
className={`
${getTypeStyles()}
p-4 rounded-lg shadow-lg
flex items-center space-x-3
transform transition-all duration-300 ease-out
${
isAnimating
? 'translate-x-0 opacity-100'
: 'translate-x-full opacity-0'
}
`}
>
{getIcon()}
<span className='text-sm font-medium'>
{message}
</span>
<button
onClick={handleClose}
className='ml-2 text-white hover:text-gray-200'
>
<svg
className='w-4 h-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
</div>
);
};
export default AnimatedToast;
アニメーション用のカスタムフック
typescript// hooks/useToast.ts
import { useState, useCallback } from 'react';
interface ToastState {
isVisible: boolean;
type: 'success' | 'error' | 'warning' | 'info';
message: string;
}
export const useToast = () => {
const [toast, setToast] = useState<ToastState>({
isVisible: false,
type: 'info',
message: '',
});
const showToast = useCallback(
(
type: 'success' | 'error' | 'warning' | 'info',
message: string
) => {
setToast({
isVisible: true,
type,
message,
});
},
[]
);
const hideToast = useCallback(() => {
setToast((prev) => ({ ...prev, isVisible: false }));
}, []);
return {
toast,
showToast,
hideToast,
};
};
アニメーション関連のよくあるエラー
typescript// ❌ エラー:アニメーションが正しく動作しない
useEffect(() => {
if (isVisible) {
setIsAnimating(true);
const timer = setTimeout(() => {
onClose(); // 直接 onClose を呼び出す
}, duration);
return () => clearTimeout(timer);
}
}, [isVisible, duration]);
// 問題:アニメーション完了前にコンポーネントが消える
// ✅ 解決策:アニメーション完了後に非表示にする
const handleClose = () => {
setIsAnimating(false);
setTimeout(() => {
onClose();
}, 300); // アニメーション時間分待つ
};
複数通知の管理システム
通知スタックの実装
複数の通知を同時に管理できるシステムを作成しましょう。
typescript// components/ToastContainer.tsx
import React from 'react';
import AnimatedToast from './AnimatedToast';
interface Toast {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
message: string;
duration?: number;
}
interface ToastContainerProps {
toasts: Toast[];
onRemove: (id: string) => void;
}
const ToastContainer: React.FC<ToastContainerProps> = ({
toasts,
onRemove,
}) => {
return (
<div className='fixed top-4 right-4 z-50 space-y-2'>
{toasts.map((toast, index) => (
<div
key={toast.id}
className='transform transition-all duration-300'
style={{
transform: `translateY(${index * 80}px)`,
}}
>
<AnimatedToast
type={toast.type}
message={toast.message}
duration={toast.duration}
isVisible={true}
onClose={() => onRemove(toast.id)}
/>
</div>
))}
</div>
);
};
export default ToastContainer;
通知管理用のカスタムフック
typescript// hooks/useToastManager.ts
import { useState, useCallback } from 'react';
interface Toast {
id: string;
type: 'success' | 'error' | 'warning' | 'info';
message: string;
duration?: number;
}
export const useToastManager = () => {
const [toasts, setToasts] = useState<Toast[]>([]);
const addToast = useCallback(
(
type: 'success' | 'error' | 'warning' | 'info',
message: string,
duration = 5000
) => {
const id = Date.now().toString();
const newToast: Toast = {
id,
type,
message,
duration,
};
setToasts((prev) => [...prev, newToast]);
// 自動削除
setTimeout(() => {
removeToast(id);
}, duration);
},
[]
);
const removeToast = useCallback((id: string) => {
setToasts((prev) =>
prev.filter((toast) => toast.id !== id)
);
}, []);
const clearAllToasts = useCallback(() => {
setToasts([]);
}, []);
return {
toasts,
addToast,
removeToast,
clearAllToasts,
};
};
使用例とエラーハンドリング
typescript// pages/index.tsx
import React from 'react';
import ToastContainer from '../components/ToastContainer';
import { useToastManager } from '../hooks/useToastManager';
const App: React.FC = () => {
const { toasts, addToast, removeToast, clearAllToasts } =
useToastManager();
const handleSuccess = () => {
addToast('success', 'データの保存が完了しました!');
};
const handleError = () => {
addToast('error', 'ネットワークエラーが発生しました');
};
const handleWarning = () => {
addToast('warning', '保存されていない変更があります');
};
return (
<div className='min-h-screen bg-gray-100 p-8'>
<div className='max-w-md mx-auto space-y-4'>
<button
onClick={handleSuccess}
className='w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600'
>
成功通知を追加
</button>
<button
onClick={handleError}
className='w-full bg-red-500 text-white py-2 px-4 rounded hover:bg-red-600'
>
エラー通知を追加
</button>
<button
onClick={handleWarning}
className='w-full bg-yellow-500 text-white py-2 px-4 rounded hover:bg-yellow-600'
>
警告通知を追加
</button>
<button
onClick={clearAllToasts}
className='w-full bg-gray-500 text-white py-2 px-4 rounded hover:bg-gray-600'
>
すべての通知をクリア
</button>
</div>
<ToastContainer
toasts={toasts}
onRemove={removeToast}
/>
</div>
);
};
export default App;
複数通知管理でのよくあるエラー
typescript// ❌ エラー:メモリリークの原因
useEffect(() => {
const timer = setTimeout(() => {
removeToast(id);
}, duration);
// クリーンアップ関数がない
}, [duration]);
// 問題:コンポーネントがアンマウントされてもタイマーが残る
// ✅ 解決策:クリーンアップ関数を追加
useEffect(() => {
const timer = setTimeout(() => {
removeToast(id);
}, duration);
return () => clearTimeout(timer);
}, [duration, id, removeToast]);
カスタマイズ可能なテーマ設計
テーマシステムの実装
プロジェクト全体で一貫した通知スタイルを維持するためのテーマシステムを作成しましょう。
typescript// types/notification.ts
export interface NotificationTheme {
success: {
background: string;
text: string;
border: string;
icon: string;
};
error: {
background: string;
text: string;
border: string;
icon: string;
};
warning: {
background: string;
text: string;
border: string;
icon: string;
};
info: {
background: string;
text: string;
border: string;
icon: string;
};
}
export interface NotificationConfig {
position:
| 'top-right'
| 'top-left'
| 'bottom-right'
| 'bottom-left'
| 'top-center'
| 'bottom-center';
maxToasts: number;
autoClose: boolean;
autoCloseDelay: number;
theme: NotificationTheme;
}
デフォルトテーマの定義
typescript// themes/defaultTheme.ts
import { NotificationTheme } from '../types/notification';
export const defaultTheme: NotificationTheme = {
success: {
background: 'bg-green-50',
text: 'text-green-800',
border: 'border-green-500',
icon: 'text-green-500',
},
error: {
background: 'bg-red-50',
text: 'text-red-800',
border: 'border-red-500',
icon: 'text-red-500',
},
warning: {
background: 'bg-yellow-50',
text: 'text-yellow-800',
border: 'border-yellow-500',
icon: 'text-yellow-500',
},
info: {
background: 'bg-blue-50',
text: 'text-blue-800',
border: 'border-blue-500',
icon: 'text-blue-500',
},
};
export const darkTheme: NotificationTheme = {
success: {
background: 'bg-green-900',
text: 'text-green-100',
border: 'border-green-400',
icon: 'text-green-400',
},
error: {
background: 'bg-red-900',
text: 'text-red-100',
border: 'border-red-400',
icon: 'text-red-400',
},
warning: {
background: 'bg-yellow-900',
text: 'text-yellow-100',
border: 'border-yellow-400',
icon: 'text-yellow-400',
},
info: {
background: 'bg-blue-900',
text: 'text-blue-100',
border: 'border-blue-400',
icon: 'text-blue-400',
},
};
テーマ対応の通知コンポーネント
typescript// components/ThemedNotification.tsx
import React from 'react';
import { NotificationTheme } from '../types/notification';
interface ThemedNotificationProps {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
theme: NotificationTheme;
isVisible: boolean;
onClose: () => void;
}
const ThemedNotification: React.FC<
ThemedNotificationProps
> = ({ type, message, theme, isVisible, onClose }) => {
if (!isVisible) return null;
const currentTheme = theme[type];
return (
<div
className={`
fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg border-l-4
${currentTheme.background} ${currentTheme.text} ${
currentTheme.border
}
transform transition-all duration-300 ease-out
${
isVisible
? 'translate-x-0 opacity-100'
: 'translate-x-full opacity-0'
}
`}
>
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-3'>
<div className={currentTheme.icon}>
{type === 'success' && (
<svg
className='w-5 h-5'
fill='currentColor'
viewBox='0 0 20 20'
>
<path
fillRule='evenodd'
d='M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z'
clipRule='evenodd'
/>
</svg>
)}
{type === 'error' && (
<svg
className='w-5 h-5'
fill='currentColor'
viewBox='0 0 20 20'
>
<path
fillRule='evenodd'
d='M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z'
clipRule='evenodd'
/>
</svg>
)}
</div>
<span className='text-sm font-medium'>
{message}
</span>
</div>
<button
onClick={onClose}
className='ml-4 text-gray-400 hover:text-gray-600'
>
<svg
className='w-4 h-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
</div>
);
};
export default ThemedNotification;
テーマ関連のよくあるエラー
typescript// ❌ エラー:テーマの型安全性が不十分
const getThemeStyles = (type: string, theme: any) => {
return theme[type]; // 型チェックがない
};
// 問題:存在しないテーマプロパティにアクセスする可能性
// ✅ 解決策:型安全なテーマアクセス
const getThemeStyles = (
type: keyof NotificationTheme,
theme: NotificationTheme
) => {
return theme[type];
};
アクセシビリティ対応
ARIA 属性の追加
アクセシビリティを考慮した通知コンポーネントを作成しましょう。
typescript// components/AccessibleNotification.tsx
import React, { useEffect, useRef } from 'react';
interface AccessibleNotificationProps {
type: 'success' | 'error' | 'warning' | 'info';
message: string;
isVisible: boolean;
onClose: () => void;
autoFocus?: boolean;
}
const AccessibleNotification: React.FC<
AccessibleNotificationProps
> = ({
type,
message,
isVisible,
onClose,
autoFocus = true,
}) => {
const notificationRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isVisible && autoFocus && notificationRef.current) {
notificationRef.current.focus();
}
}, [isVisible, autoFocus]);
const getRole = () => {
switch (type) {
case 'error':
return 'alert';
case 'warning':
return 'alert';
default:
return 'status';
}
};
const getAriaLive = () => {
switch (type) {
case 'error':
return 'assertive';
case 'warning':
return 'assertive';
default:
return 'polite';
}
};
if (!isVisible) return null;
return (
<div
ref={notificationRef}
role={getRole()}
aria-live={getAriaLive()}
aria-atomic='true'
tabIndex={-1}
className={`
fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg border-l-4
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2
${
type === 'success'
? 'bg-green-50 border-green-500 text-green-800'
: ''
}
${
type === 'error'
? 'bg-red-50 border-red-500 text-red-800'
: ''
}
${
type === 'warning'
? 'bg-yellow-50 border-yellow-500 text-yellow-800'
: ''
}
${
type === 'info'
? 'bg-blue-50 border-blue-500 text-blue-800'
: ''
}
`}
>
<div className='flex items-center justify-between'>
<div className='flex items-center space-x-3'>
<span className='text-sm font-medium'>
{message}
</span>
</div>
<button
onClick={onClose}
aria-label='通知を閉じる'
className='ml-4 text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-gray-500 rounded'
>
<svg
className='w-4 h-4'
fill='none'
stroke='currentColor'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</button>
</div>
</div>
);
};
export default AccessibleNotification;
キーボードナビゲーション対応
typescript// hooks/useKeyboardNavigation.ts
import { useEffect } from 'react';
export const useKeyboardNavigation = (
isVisible: boolean,
onClose: () => void
) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (!isVisible) return;
switch (event.key) {
case 'Escape':
event.preventDefault();
onClose();
break;
case 'Enter':
event.preventDefault();
onClose();
break;
}
};
if (isVisible) {
document.addEventListener('keydown', handleKeyDown);
}
return () => {
document.removeEventListener(
'keydown',
handleKeyDown
);
};
}, [isVisible, onClose]);
};
スクリーンリーダー対応
typescript// components/ScreenReaderAnnouncement.tsx
import React, { useEffect } from 'react';
interface ScreenReaderAnnouncementProps {
message: string;
isVisible: boolean;
priority?: 'polite' | 'assertive';
}
const ScreenReaderAnnouncement: React.FC<
ScreenReaderAnnouncementProps
> = ({ message, isVisible, priority = 'polite' }) => {
useEffect(() => {
if (isVisible && message) {
// スクリーンリーダーにメッセージを読み上げさせる
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', priority);
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
// 少し待ってから削除
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
}, [isVisible, message, priority]);
return null;
};
export default ScreenReaderAnnouncement;
アクセシビリティ関連のよくあるエラー
typescript// ❌ エラー:アクセシビリティが考慮されていない
<div className="notification">
<span>{message}</span>
<button onClick={onClose}>×</button>
</div>
// 問題:
// - スクリーンリーダーが通知の存在を認識できない
// - キーボードナビゲーションができない
// - ボタンに適切なラベルがない
// ✅ 解決策:アクセシビリティを考慮した実装
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
tabIndex={-1}
className="notification"
>
<span>{message}</span>
<button
onClick={onClose}
aria-label="通知を閉じる"
onKeyDown={(e) => e.key === 'Enter' && onClose()}
>
×
</button>
</div>
まとめ
この記事では、Tailwind CSS を使用した通知・トースト UI の実装について、段階的に学んできました。基本的な通知コンポーネントから始まり、アニメーション、複数通知の管理、テーマシステム、そしてアクセシビリティ対応まで、実用的な実装例を紹介しました。
重要なポイント
-
段階的な実装: シンプルなものから始めて、徐々に機能を追加していくことで、理解しやすく保守性の高いコードを書けます。
-
エラーハンドリング: 実際の開発で遭遇する可能性の高いエラーとその解決策を事前に知っておくことで、開発効率が大幅に向上します。
-
アクセシビリティ: すべてのユーザーが快適に使えるアプリケーションを作るためには、アクセシビリティを最初から考慮した設計が重要です。
-
再利用性: カスタムフックやテーマシステムを活用することで、プロジェクト全体で一貫した通知システムを実現できます。
次のステップ
この記事で学んだ内容を基に、あなたのプロジェクトに通知システムを実装してみてください。最初はシンプルなものから始めて、徐々に機能を追加していくことをお勧めします。
また、実際のユーザーフィードバックを収集して、通知の表示タイミングやメッセージの内容を改善していくことで、より良いユーザー体験を提供できるようになります。
通知システムは、アプリケーションの使いやすさを大きく左右する要素です。適切に実装することで、ユーザーが安心してアプリケーションを使用できる環境を作り出せます。
関連リンク
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来