モダンUIコンポーネントの実装パターン
実装ガイドReact、Vue、Angularで共通して使えるUIパターンを解説
状態や進捗を視覚的に表示するバッジコンポーネント。20以上のステータスタイプと6つのバリアント、アニメーション効果に対応し、ダッシュボードやリスト表示に最適
solid
outline
soft
dot
pill
minimal
成功、エラー、警告、情報から、アクティブ、オンライン、プレミアムまで20以上のステータスタイプを内蔵。各タイプに最適化されたアイコンとカラーを自動設定
solid、outline、soft、dot、pill、minimalの6つのバリアントを提供。用途に応じて最適なスタイルを選択可能
クリック可能、アニメーション、パルス効果、ツールチップ表示に対応。ユーザーエンゲージメントを高める動的な体験を提供
プロンプト例:
ユーザー管理画面でユーザーの状態を表示するステータスバッジを作成してください。オンライン、オフライン、取り込み中、離席中の4つの状態で、dotバリアントを使用してコンパクトに表示してください
プロンプト例:
プロジェクト管理ツールでタスクの進捗状況を表示するバッジを作成してください。下書き、進行中、レビュー中、完了、アーカイブの状態で、pillバリアントとアニメーション効果を使用してください
プロンプト例:
オンラインショップの商品管理画面で使用する在庫状況バッジを作成してください。在庫あり、残り僅か、在庫切れ、入荷予定の状態で、solidバリアントを使用し、緊急度に応じて色分けしてください
import React from 'react';
export type StatusType =
| 'success' | 'error' | 'warning' | 'info' | 'pending'
| 'active' | 'inactive' | 'draft' | 'published' | 'archived'
| 'online' | 'offline' | 'busy' | 'away' | 'new' | 'hot'
| 'premium' | 'free' | 'trial' | 'expired' | 'custom';
export interface StatusBadgeProps {
/** ステータスタイプ */
status: StatusType;
/** 表示テキスト(指定しない場合は自動設定) */
text?: string;
/** バッジのサイズ */
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/** バッジのバリアント */
variant?: 'solid' | 'outline' | 'soft' | 'dot' | 'pill' | 'minimal';
/** アニメーション */
animated?: boolean;
/** パルスアニメーション(点滅) */
pulse?: boolean;
/** アイコンの表示 */
showIcon?: boolean;
/** カスタムアイコン */
customIcon?: React.ReactNode;
/** カスタムカラー */
customColor?: {
bg: string;
text: string;
border?: string;
};
/** 大文字変換 */
uppercase?: boolean;
/** 角丸の調整 */
rounded?: 'none' | 'sm' | 'md' | 'lg' | 'full';
/** クリック可能 */
clickable?: boolean;
/** クリックイベント */
onClick?: () => void;
/** ツールチップテキスト */
tooltip?: string;
/** カスタムクラス */
className?: string;
}
const StatusBadge: React.FC<StatusBadgeProps> = ({
status,
text,
size = 'md',
variant = 'solid',
animated = false,
pulse = false,
showIcon = true,
customIcon,
customColor,
uppercase = false,
rounded = 'md',
clickable = false,
onClick,
tooltip,
className = ''
}) => {
// デフォルトテキスト設定
const defaultTexts: Record<StatusType, string> = {
success: '成功',
error: 'エラー',
warning: '警告',
info: '情報',
pending: '保留中',
active: 'アクティブ',
inactive: '非アクティブ',
draft: '下書き',
published: '公開済み',
archived: 'アーカイブ',
online: 'オンライン',
offline: 'オフライン',
busy: '取り込み中',
away: '離席中',
new: '新着',
hot: '人気',
premium: 'プレミアム',
free: '無料',
trial: 'トライアル',
expired: '期限切れ',
custom: 'カスタム'
};
// ステータス別カラー設定
const statusColors: Record<StatusType, { bg: string; text: string; border: string; dot: string }> = {
success: { bg: 'bg-green-500', text: 'text-green-700', border: 'border-green-500', dot: 'bg-green-400' },
error: { bg: 'bg-red-500', text: 'text-red-700', border: 'border-red-500', dot: 'bg-red-400' },
warning: { bg: 'bg-yellow-500', text: 'text-yellow-700', border: 'border-yellow-500', dot: 'bg-yellow-400' },
info: { bg: 'bg-blue-500', text: 'text-blue-700', border: 'border-blue-500', dot: 'bg-blue-400' },
pending: { bg: 'bg-orange-500', text: 'text-orange-700', border: 'border-orange-500', dot: 'bg-orange-400' },
active: { bg: 'bg-green-500', text: 'text-green-700', border: 'border-green-500', dot: 'bg-green-400' },
inactive: { bg: 'bg-gray-500', text: 'text-gray-700', border: 'border-gray-500', dot: 'bg-gray-400' },
draft: { bg: 'bg-gray-500', text: 'text-gray-700', border: 'border-gray-500', dot: 'bg-gray-400' },
published: { bg: 'bg-green-500', text: 'text-green-700', border: 'border-green-500', dot: 'bg-green-400' },
archived: { bg: 'bg-purple-500', text: 'text-purple-700', border: 'border-purple-500', dot: 'bg-purple-400' },
online: { bg: 'bg-green-500', text: 'text-green-700', border: 'border-green-500', dot: 'bg-green-400' },
offline: { bg: 'bg-gray-500', text: 'text-gray-700', border: 'border-gray-500', dot: 'bg-gray-400' },
busy: { bg: 'bg-red-500', text: 'text-red-700', border: 'border-red-500', dot: 'bg-red-400' },
away: { bg: 'bg-yellow-500', text: 'text-yellow-700', border: 'border-yellow-500', dot: 'bg-yellow-400' },
new: { bg: 'bg-blue-500', text: 'text-blue-700', border: 'border-blue-500', dot: 'bg-blue-400' },
hot: { bg: 'bg-red-500', text: 'text-red-700', border: 'border-red-500', dot: 'bg-red-400' },
premium: { bg: 'bg-purple-500', text: 'text-purple-700', border: 'border-purple-500', dot: 'bg-purple-400' },
free: { bg: 'bg-gray-500', text: 'text-gray-700', border: 'border-gray-500', dot: 'bg-gray-400' },
trial: { bg: 'bg-blue-500', text: 'text-blue-700', border: 'border-blue-500', dot: 'bg-blue-400' },
expired: { bg: 'bg-red-500', text: 'text-red-700', border: 'border-red-500', dot: 'bg-red-400' },
custom: { bg: 'bg-gray-500', text: 'text-gray-700', border: 'border-gray-500', dot: 'bg-gray-400' }
};
// サイズ設定
const sizeClasses = {
xs: {
container: 'px-2 py-0.5 text-xs',
icon: 'w-2.5 h-2.5',
dot: 'w-1.5 h-1.5',
text: 'text-xs'
},
sm: {
container: 'px-2.5 py-1 text-xs',
icon: 'w-3 h-3',
dot: 'w-2 h-2',
text: 'text-xs'
},
md: {
container: 'px-3 py-1.5 text-sm',
icon: 'w-4 h-4',
dot: 'w-2.5 h-2.5',
text: 'text-sm'
},
lg: {
container: 'px-4 py-2 text-base',
icon: 'w-5 h-5',
dot: 'w-3 h-3',
text: 'text-base'
},
xl: {
container: 'px-5 py-2.5 text-lg',
icon: 'w-6 h-6',
dot: 'w-4 h-4',
text: 'text-lg'
}
};
// 角丸設定
const roundedClasses = {
none: 'rounded-none',
sm: 'rounded-sm',
md: 'rounded-md',
lg: 'rounded-lg',
full: 'rounded-full'
};
// アイコン設定
const getIcon = () => {
if (customIcon) return customIcon;
const iconProps = { className: sizeClasses[size].icon };
switch (status) {
case 'success':
case 'active':
case 'published':
case 'online':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
);
case 'error':
case 'expired':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
);
case 'warning':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
);
case 'info':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
case 'pending':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
case 'new':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z" />
</svg>
);
case 'hot':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z" />
</svg>
);
case 'premium':
return (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3v4M3 5h4M6 17v4m-2-2h4m5-16l2.286 6.857L21 12l-5.714 2.143L13 21l-2.286-6.857L5 12l5.714-2.143L13 3z" />
</svg>
);
default:
return variant === 'dot' ? null : (
<svg {...iconProps} fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
);
}
};
// カラー設定(カスタムカラーまたはデフォルト)
const colors = customColor || statusColors[status];
const displayText = text || defaultTexts[status];
// バリアント別スタイル
const getVariantClasses = () => {
const baseClasses = `inline-flex items-center font-medium transition-all duration-200 ${sizeClasses[size].container} ${roundedClasses[rounded]}`;
switch (variant) {
case 'solid':
return `${baseClasses} ${colors.bg} text-white`;
case 'outline':
return `${baseClasses} border-2 ${colors.border} ${colors.text} bg-transparent`;
case 'soft':
return `${baseClasses} ${colors.bg} bg-opacity-10 ${colors.text}`;
case 'dot':
return `${baseClasses} ${colors.text} bg-gray-100`;
case 'pill':
return `${baseClasses} ${colors.bg} text-white rounded-full`;
case 'minimal':
return `${baseClasses} ${colors.text} bg-transparent`;
default:
return `${baseClasses} ${colors.bg} text-white`;
}
};
// アニメーションクラス
const getAnimationClasses = () => {
let classes = '';
if (animated) classes += ' hover:scale-105 hover:shadow-lg';
if (pulse) classes += ' animate-pulse';
if (clickable) classes += ' cursor-pointer hover:opacity-80';
return classes;
};
const Component = clickable ? 'button' : 'span';
return (
<Component
onClick={clickable ? onClick : undefined}
title={tooltip}
className={`${getVariantClasses()} ${getAnimationClasses()} ${className}`}
>
{/* ドットバリアント専用のドット */}
{variant === 'dot' && (
<span
className={`${sizeClasses[size].dot} ${colors.dot} rounded-full mr-2 ${pulse ? 'animate-pulse' : ''}`}
/>
)}
{/* アイコン表示 */}
{showIcon && variant !== 'dot' && (
<span className="mr-1.5">
{getIcon()}
</span>
)}
{/* テキスト表示 */}
<span className={uppercase ? 'uppercase' : ''}>
{displayText}
</span>
</Component>
);
};
export default StatusBadge;
// 基本的な使用例
import StatusBadge from './status-badge';
// シンプルなステータス表示
<StatusBadge status="success" />
<StatusBadge status="error" />
<StatusBadge status="warning" />
<StatusBadge status="info" />
// カスタムテキストとサイズ
<StatusBadge
status="active"
text="アクティブ"
size="lg"
/>
// バリアントの使い分け
<StatusBadge status="online" variant="dot" size="sm" />
<StatusBadge status="premium" variant="pill" />
<StatusBadge status="pending" variant="outline" />
<StatusBadge status="published" variant="soft" />
// アニメーション効果
<StatusBadge
status="new"
animated={true}
pulse={true}
/>
// インタラクティブなバッジ
<StatusBadge
status="draft"
clickable={true}
tooltip="クリックして編集"
onClick={() => handleEdit()}
/>
// ユーザー管理での使用例
function UserStatusIndicator({ user }) {
const getStatus = () => {
if (user.isOnline) return 'online';
if (user.lastSeen < 5) return 'away';
return 'offline';
};
return (
<div className="flex items-center space-x-2">
<img src={user.avatar} className="w-8 h-8 rounded-full" />
<span className="font-medium">{user.name}</span>
<StatusBadge
status={getStatus()}
variant="dot"
size="sm"
/>
</div>
);
}
// プロジェクト管理での使用例
function TaskItem({ task }) {
const statusMapping = {
'todo': 'pending',
'in_progress': 'active',
'review': 'warning',
'done': 'success',
'archived': 'archived'
};
return (
<div className="flex justify-between items-center p-4 border rounded">
<span>{task.title}</span>
<StatusBadge
status={statusMapping[task.status]}
variant="soft"
clickable={true}
onClick={() => updateTaskStatus(task.id)}
/>
</div>
);
}
// カスタムカラーの使用
<StatusBadge
status="custom"
text="カスタム状態"
customColor={{
bg: 'bg-indigo-500',
text: 'text-indigo-700',
border: 'border-indigo-500'
}}
customIcon={<CustomIcon />}
/>
// 通知カウンター的な使用
<StatusBadge
status="new"
text="3"
variant="solid"
size="xs"
rounded="full"
className="absolute -top-2 -right-2"
/>