ステータスバッジ - Vibe Coding Showcase

ステータスバッジ

状態や進捗を視覚的に表示するバッジコンポーネント。20以上のステータスタイプと6つのバリアント、アニメーション効果に対応し、ダッシュボードやリスト表示に最適

デザインプレビュー

バリアント

サイズ

オプション

基本ステータス

ユーザー状態

コンテンツ状態

特別なステータス

サイズ比較

XSSMMDLGXL

バリアント比較

solid

solid

outline

outline

soft

soft

dot

dot

pill

pill

minimal

minimal

実用例

ユーザー管理

田中太郎オンライン
佐藤花子離席中
山田次郎取り込み中
鈴木美咲オフライン

プロジェクト管理

デザインレビュー保留中
開発作業アクティブ
テスト実行成功
デプロイメントエラー

商品管理

iPhone 15 Proアクティブ
MacBook Air人気
iPad Pro新着
Apple Watch非アクティブ

💡 使用例

ダッシュボード

  • • ユーザーのオンライン状態表示
  • • システムの稼働状況
  • • タスクの進捗管理

コンテンツ管理

  • • 記事の公開状態
  • • 商品の在庫状況
  • • コメントの承認状態

ユーザー体験

  • • 通知やアラート
  • • 機能の利用可能性
  • • プランやステータス表示

ステータスバッジの特徴

豊富なステータスタイプ

成功、エラー、警告、情報から、アクティブ、オンライン、プレミアムまで20以上のステータスタイプを内蔵。各タイプに最適化されたアイコンとカラーを自動設定

多様なデザインバリアント

solid、outline、soft、dot、pill、minimalの6つのバリアントを提供。用途に応じて最適なスタイルを選択可能

インタラクティブ機能

クリック可能、アニメーション、パルス効果、ツールチップ表示に対応。ユーザーエンゲージメントを高める動的な体験を提供

AI活用ガイド

ユーザー管理ダッシュボード

プロンプト例:

ユーザー管理画面でユーザーの状態を表示するステータスバッジを作成してください。オンライン、オフライン、取り込み中、離席中の4つの状態で、dotバリアントを使用してコンパクトに表示してください

プロジェクト管理システム

プロンプト例:

プロジェクト管理ツールでタスクの進捗状況を表示するバッジを作成してください。下書き、進行中、レビュー中、完了、アーカイブの状態で、pillバリアントとアニメーション効果を使用してください

Eコマース商品管理

プロンプト例:

オンラインショップの商品管理画面で使用する在庫状況バッジを作成してください。在庫あり、残り僅か、在庫切れ、入荷予定の状態で、solidバリアントを使用し、緊急度に応じて色分けしてください

実装コード

ファイルサイズ: 12.8KB TypeScript

コンポーネント実装

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"
/>