モダンUIコンポーネントの実装パターン
実装ガイドReact、Vue、Angularで共通して使えるUIパターンを解説
料金プランを魅力的に表示するカードコンポーネント。複数のテーマ、サイズ、機能比較表示に対応し、SaaSやECサイトでの料金ページ作成に最適
個人利用や小規模プロジェクトに最適
ビジネス利用におすすめの人気プラン
大規模組織向けの包括的ソリューション
月額・年額切り替え、割引価格表示、無料プラン対応など、様々な料金体系に対応。通貨や課金サイクルも自由にカスタマイズ可能
機能の有無を視覚的に表示し、制限や詳細説明も追加可能。ユーザーが各プランの違いを一目で理解できる設計
5つのテーマ(デフォルト、モダン、ミニマル、グラデーション、ダーク)を提供。人気プランの強調表示やホバーエフェクトも内蔵
プロンプト例:
3つの料金プラン(フリー、プロ、エンタープライズ)を作成してください。月額と年額の切り替え機能付きで、プロプランを人気プランとして強調表示。モダンテーマを使用してください
プロンプト例:
オンラインショップ向けの配送プラン比較カードを作成してください。スタンダード、プレミアム、エクスプレスの3プランで、配送日数や保険などの機能比較を含めてください
プロンプト例:
ビジネスコンサルティング会社向けの料金プランを作成してください。時間制、プロジェクト制、月額制の3つのプランで、グラデーションテーマを使用してプレミアム感を演出してください
import React, { useState } from 'react';
export interface PricingFeature {
/** 機能名 */
name: string;
/** 含まれているかどうか */
included: boolean;
/** 追加の説明 */
description?: string;
/** 制限がある場合(例:「月5回まで」) */
limit?: string;
}
export interface PricingPlan {
/** プラン名 */
name: string;
/** 説明 */
description: string;
/** 価格 */
price: number;
/** 通貨 */
currency?: string;
/** 課金サイクル */
billingCycle?: 'monthly' | 'yearly' | 'one-time';
/** 割引価格(年間プランなど) */
discountPrice?: number;
/** 人気プランかどうか */
popular?: boolean;
/** 機能リスト */
features: PricingFeature[];
/** CTAボタンテキスト */
buttonText: string;
/** CTAボタンのリンク */
buttonLink?: string;
/** 無料プランかどうか */
isFree?: boolean;
/** プランの詳細ページリンク */
detailsLink?: string;
}
export interface PricingCardProps {
/** プラン情報 */
plan: PricingPlan;
/** テーマ */
theme?: 'default' | 'modern' | 'minimal' | 'gradient' | 'dark';
/** サイズ */
size?: 'sm' | 'md' | 'lg';
/** 年間/月間切り替え */
billingToggle?: boolean;
/** 現在の課金サイクル */
currentBilling?: 'monthly' | 'yearly';
/** CTAボタンクリック時のコールバック */
onButtonClick?: (plan: PricingPlan) => void;
/** 詳細リンククリック時のコールバック */
onDetailsClick?: (plan: PricingPlan) => void;
/** カスタムクラス */
className?: string;
}
const PricingCard: React.FC<PricingCardProps> = ({
plan,
theme = 'default',
size = 'md',
billingToggle = false,
currentBilling = 'monthly',
onButtonClick,
onDetailsClick,
className = ''
}) => {
const [hovering, setHovering] = useState(false);
// テーマ設定
const themes = {
default: {
card: 'bg-white border border-gray-200 shadow-lg',
popularBadge: 'bg-blue-500 text-white',
header: 'bg-gray-50',
price: 'text-gray-900',
originalPrice: 'text-gray-500 line-through',
description: 'text-gray-600',
feature: 'text-gray-700',
featureIcon: 'text-green-500',
notIncluded: 'text-gray-400',
button: plan.popular
? 'bg-blue-500 hover:bg-blue-600 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-900',
detailsLink: 'text-blue-500 hover:text-blue-600'
},
modern: {
card: 'bg-white border border-gray-200 shadow-xl rounded-2xl',
popularBadge: 'bg-gradient-to-r from-purple-500 to-pink-500 text-white',
header: 'bg-gradient-to-br from-gray-50 to-gray-100',
price: 'text-gray-900',
originalPrice: 'text-gray-500 line-through',
description: 'text-gray-600',
feature: 'text-gray-700',
featureIcon: 'text-green-500',
notIncluded: 'text-gray-400',
button: plan.popular
? 'bg-gradient-to-r from-purple-500 to-pink-500 hover:from-purple-600 hover:to-pink-600 text-white'
: 'bg-gray-100 hover:bg-gray-200 text-gray-900',
detailsLink: 'text-purple-500 hover:text-purple-600'
},
minimal: {
card: 'bg-white border border-gray-100 shadow-sm',
popularBadge: 'bg-gray-900 text-white',
header: 'bg-transparent',
price: 'text-gray-900',
originalPrice: 'text-gray-400 line-through',
description: 'text-gray-500',
feature: 'text-gray-600',
featureIcon: 'text-gray-900',
notIncluded: 'text-gray-300',
button: plan.popular
? 'bg-gray-900 hover:bg-gray-800 text-white'
: 'bg-gray-50 hover:bg-gray-100 text-gray-900 border border-gray-200',
detailsLink: 'text-gray-500 hover:text-gray-700'
},
gradient: {
card: plan.popular
? 'bg-gradient-to-br from-blue-500 to-purple-600 text-white border-0 shadow-2xl'
: 'bg-white border border-gray-200 shadow-lg',
popularBadge: 'bg-yellow-400 text-gray-900',
header: plan.popular ? 'bg-white bg-opacity-10' : 'bg-gray-50',
price: plan.popular ? 'text-white' : 'text-gray-900',
originalPrice: plan.popular ? 'text-blue-200 line-through' : 'text-gray-500 line-through',
description: plan.popular ? 'text-blue-100' : 'text-gray-600',
feature: plan.popular ? 'text-white' : 'text-gray-700',
featureIcon: plan.popular ? 'text-blue-200' : 'text-green-500',
notIncluded: plan.popular ? 'text-blue-300' : 'text-gray-400',
button: plan.popular
? 'bg-white text-blue-600 hover:bg-gray-100'
: 'bg-blue-500 hover:bg-blue-600 text-white',
detailsLink: plan.popular ? 'text-blue-200 hover:text-white' : 'text-blue-500 hover:text-blue-600'
},
dark: {
card: 'bg-gray-900 border border-gray-700 shadow-xl text-white',
popularBadge: 'bg-blue-500 text-white',
header: 'bg-gray-800',
price: 'text-white',
originalPrice: 'text-gray-400 line-through',
description: 'text-gray-300',
feature: 'text-gray-200',
featureIcon: 'text-green-400',
notIncluded: 'text-gray-500',
button: plan.popular
? 'bg-blue-500 hover:bg-blue-600 text-white'
: 'bg-gray-700 hover:bg-gray-600 text-white',
detailsLink: 'text-blue-400 hover:text-blue-300'
}
};
// サイズ設定
const sizes = {
sm: {
card: 'p-4',
header: 'p-4 pb-2',
body: 'p-4 pt-2',
price: 'text-2xl',
planName: 'text-lg',
description: 'text-sm',
feature: 'text-sm',
button: 'py-2 px-4 text-sm'
},
md: {
card: 'p-6',
header: 'p-6 pb-4',
body: 'p-6 pt-4',
price: 'text-4xl',
planName: 'text-xl',
description: 'text-base',
feature: 'text-sm',
button: 'py-3 px-6 text-base'
},
lg: {
card: 'p-8',
header: 'p-8 pb-6',
body: 'p-8 pt-6',
price: 'text-5xl',
planName: 'text-2xl',
description: 'text-lg',
feature: 'text-base',
button: 'py-4 px-8 text-lg'
}
};
const themeConfig = themes[theme];
const sizeConfig = sizes[size];
// 現在の価格を取得
const getCurrentPrice = () => {
if (plan.isFree) return 0;
if (currentBilling === 'yearly' && plan.discountPrice) {
return plan.discountPrice;
}
return plan.price;
};
// 元の価格を取得(割引がある場合)
const getOriginalPrice = () => {
if (currentBilling === 'yearly' && plan.discountPrice && plan.discountPrice < plan.price) {
return plan.price;
}
return null;
};
// 課金サイクルテキスト
const getBillingText = () => {
switch (plan.billingCycle || currentBilling) {
case 'yearly':
return '/年';
case 'monthly':
return '/月';
case 'one-time':
return '買い切り';
default:
return '/月';
}
};
const currentPrice = getCurrentPrice();
const originalPrice = getOriginalPrice();
return (
<div
className={`relative rounded-lg transition-all duration-300 ${themeConfig.card} ${sizeConfig.card} ${className} ${
hovering && !plan.popular ? 'transform scale-105 shadow-xl' : ''
} ${plan.popular ? 'ring-2 ring-blue-500 ring-opacity-50' : ''}`}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
{/* 人気プランバッジ */}
{plan.popular && (
<div className="absolute -top-3 left-1/2 transform -translate-x-1/2">
<div className={`${themeConfig.popularBadge} px-4 py-1 rounded-full text-xs font-semibold`}>
人気プラン
</div>
</div>
)}
{/* ヘッダー */}
<div className={`${themeConfig.header} ${sizeConfig.header} rounded-t-lg`}>
<h3 className={`${sizeConfig.planName} font-bold ${theme === 'gradient' && plan.popular ? 'text-white' : 'text-gray-900'}`}>
{plan.name}
</h3>
<p className={`${sizeConfig.description} ${themeConfig.description} mt-1`}>
{plan.description}
</p>
</div>
{/* 価格セクション */}
<div className={`${sizeConfig.body}`}>
<div className="flex items-baseline justify-center mb-6">
{plan.isFree ? (
<span className={`${sizeConfig.price} font-bold ${themeConfig.price}`}>
無料
</span>
) : (
<>
<span className={`${sizeConfig.price} font-bold ${themeConfig.price}`}>
{plan.currency || '¥'}{currentPrice.toLocaleString()}
</span>
<span className={`ml-1 ${sizeConfig.feature} ${themeConfig.description}`}>
{getBillingText()}
</span>
{originalPrice && (
<span className={`ml-2 ${sizeConfig.feature} ${themeConfig.originalPrice}`}>
{plan.currency || '¥'}{originalPrice.toLocaleString()}
</span>
)}
</>
)}
</div>
{/* 機能リスト */}
<ul className="space-y-3 mb-8">
{plan.features.map((feature, index) => (
<li key={index} className="flex items-start">
<svg
className={`w-5 h-5 mt-0.5 mr-3 flex-shrink-0 ${
feature.included ? themeConfig.featureIcon : themeConfig.notIncluded
}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
{feature.included ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
)}
</svg>
<div className="flex-1">
<span className={`${sizeConfig.feature} ${
feature.included ? themeConfig.feature : themeConfig.notIncluded
}`}>
{feature.name}
</span>
{feature.limit && (
<span className={`ml-2 text-xs ${themeConfig.description}`}>
({feature.limit})
</span>
)}
{feature.description && (
<p className={`text-xs ${themeConfig.description} mt-1`}>
{feature.description}
</p>
)}
</div>
</li>
))}
</ul>
{/* CTAボタン */}
<button
onClick={() => onButtonClick?.(plan)}
className={`w-full ${sizeConfig.button} font-semibold rounded-lg transition-colors duration-200 ${themeConfig.button}`}
>
{plan.buttonText}
</button>
{/* 詳細リンク */}
{plan.detailsLink && (
<div className="text-center mt-4">
<button
onClick={() => onDetailsClick?.(plan)}
className={`${sizeConfig.feature} ${themeConfig.detailsLink} hover:underline`}
>
詳細を見る
</button>
</div>
)}
</div>
</div>
);
};
export default PricingCard;
// 基本的な使用例
import PricingCard from './pricing-card';
const basicPlan = {
name: 'ベーシック',
description: '個人利用に最適なプラン',
price: 980,
currency: '¥',
billingCycle: 'monthly',
features: [
{ name: '月10,000リクエスト', included: true },
{ name: '基本サポート', included: true },
{ name: 'APIアクセス', included: false },
{ name: '優先サポート', included: false }
],
buttonText: '開始する',
isFree: false
};
function App() {
const handlePlanSelect = (plan) => {
console.log('プラン選択:', plan.name);
// 決済処理やサインアップフローへ
};
return (
<PricingCard
plan={basicPlan}
theme="default"
size="md"
onButtonClick={handlePlanSelect}
/>
);
}
// 人気プランの強調表示
const popularPlan = {
name: 'プロフェッショナル',
description: 'ビジネス利用におすすめ',
price: 2980,
discountPrice: 2480, // 年額プラン時の割引価格
popular: true, // 人気プランとして強調
features: [
{ name: '月100,000リクエスト', included: true },
{ name: '優先サポート', included: true },
{ name: 'APIアクセス', included: true },
{ name: 'カスタム統合', included: true, limit: '月3回まで' }
],
buttonText: '今すぐ始める',
detailsLink: '/plans/pro'
};
// 年額・月額切り替え機能付き
function PricingSection() {
const [billing, setBilling] = useState('monthly');
return (
<div className="grid md:grid-cols-3 gap-8">
{plans.map(plan => (
<PricingCard
key={plan.name}
plan={plan}
theme="modern"
currentBilling={billing}
billingToggle={true}
onButtonClick={handlePlanSelect}
/>
))}
</div>
);
}
// 無料プランの例
const freePlan = {
name: 'フリー',
description: '無料で始められる',
price: 0,
isFree: true,
features: [
{ name: '月1,000リクエスト', included: true },
{ name: 'コミュニティサポート', included: true },
{ name: 'APIアクセス', included: false },
{ name: '商用利用', included: false }
],
buttonText: '無料で始める'
};
// グラデーションテーマでプレミアム感を演出
<PricingCard
plan={premiumPlan}
theme="gradient"
size="lg"
onButtonClick={handlePlanSelect}
/>