前回の記事に追加してBadgeを追加してみます。
完成イメージ
ディレクトリ構成
└── ReactAppProjekuto/
└── src/
├── chakraComponents/
│ └── ui/
│ └── CardList.tsx
├── framerMotionComponents/
│ └── fadeinWithScroll.tsx
├── App.tsx
└── main.tsx
実装
ポイント
■背景グラデーション: backgroundImage="linear-gradient(to right, #ff9a36, #ff6b00)"
■テキスト色: color="white"
■丸み: borderRadius="full" で pill型(カプセル形)に
■影: boxShadow でやや浮かせて強調
■条件付き表示: i === 1 && ... で2番目のカードだけ出す
CardList.tsx
import { Box, SimpleGrid, Heading, Text, Card, Button, VStack,Stack,Badge,HStack } from '@chakra-ui/react';
import { FadeIn, FadeInStagger } from '@/framerMotionComponents/fadeInWithScroll';
import { motion } from 'framer-motion';
import { FaCrown, FaClock, FaEnvelope, FaStar, FaPhone } from 'react-icons/fa';
import type{ ReactElement } from 'react'; // ← ★ これを追加
// 各特徴(feature) の型
type Feature = {
icon: ReactElement; // JSX.Element でもOKだが ReactElement のほうが一般的
text: string;
};
// 各カードの型
type CardItem = {
title: string;
subtitle: string;
price: string;
plan: string;
features: Feature[];
};
//
// --- 💎 データ配列 ---
//
const cards: CardItem[] = [
{
title: '無料プラン',
subtitle: '初回限定お試し',
price: '0円 / 無料',
plan: '無料お試しプラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '3回分の無料クレジット' },
{ icon: <FaClock color="#63b3ed" />, text: '登録後すぐに利用可能' },
{ icon: <FaStar color="#f6ad55" />, text: '高度編集機能' },
{ icon: <FaEnvelope color="#48bb78" />, text: '優先サポート' },
],
},
{
title: '基本プラン',
subtitle: '個人向け',
price: '1,000円 / 月',
plan: '基本プラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '月30ポイント付与' },
{ icon: <FaEnvelope color="#48bb78" />, text: 'メールサポート付き' },
{ icon: <FaClock color="#63b3ed" />, text: '高速生成' },
],
},
{
title: 'クリエイタープラン',
subtitle: '法人向け',
price: '2,000円 / 月',
plan: 'クリエイタープラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '月100ポイント付与' },
{ icon: <FaEnvelope color="#48bb78" />, text: 'チャットサポート付き' },
{ icon: <FaClock color="#63b3ed" />, text: '優先サポート' },
],
},
{
title: 'プロプラン',
subtitle: '大企業向け',
price: '3,000円 / 月',
plan: 'プロプラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '月10000ポイント付与' },
{ icon: <FaPhone color="#48bb78" />, text: '電話対応付き' },
{ icon: <FaEnvelope color="#48bb78" />, text: 'チャットサポート付き' },
{ icon: <FaClock color="#63b3ed" />, text: '優先サポート' },
],
},
];
/*
const cards = [
{
title: '無料プラン',
subtitle: '初回限定お試し',
price: '0円 / 無料',
plan: '無料お試しプラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '3回分の無料クレジット' },
{ icon: <FaClock color="#63b3ed" />, text: '登録後すぐに利用可能' },
{ icon: <FaStar color="#f6ad55" />, text: '高度編集機能' },
{ icon: <FaEnvelope color="#48bb78" />, text: '優先サポート' },
],
},
{
title: '基本プラン',
subtitle: '個人向け',
price: '1,000円 / 月',
plan: '基本プラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '月30ポイント付与' },
{ icon: <FaEnvelope color="#48bb78" />, text: 'メールサポート付き' },
{ icon: <FaClock color="#63b3ed" />, text: '高速生成' },
],
},
{
title: 'クリエイタープラン',
subtitle: '法人向け',
price: '2,000円 / 月',
plan: 'クリエイタープラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '月100ポイント付与' },
{ icon: <FaEnvelope color="#48bb78" />, text: 'チャットサポート付き' },
{ icon: <FaClock color="#63b3ed" />, text: '優先サポート' },
],
},
{
title: 'プロプラン',
subtitle: '大企業向け',
price: '3,000円 / 月',
plan: 'プロプラン',
features: [
{ icon: <FaStar color="#f6ad55" />, text: '月10000ポイント付与' },
{ icon: <FaPhone color="#48bb78" />, text: '電話対応付き' },
{ icon: <FaEnvelope color="#48bb78" />, text: 'チャットサポート付き' },
{ icon: <FaClock color="#63b3ed" />, text: '優先サポート' },
],
},
];
*/
// --- コンポーネント ---
export const CardList = () => {
return (
<Box p={6}>
<FadeInStagger>
<SimpleGrid columns={[1, 2, 2, 4]}>
{cards.map((card, i) => (
<FadeIn key={i}>
<motion.div whileHover={{ scale: 1.05 }} transition={{ duration: 0.3 }}>
<Card.Root
boxShadow="md"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
p={4}
bg="white"
position="relative" // ← Badge配置のため
_hover={{ boxShadow: 'xl' }}
mr={8}
>
{/* ✅ 2番目カードにだけBadgeを追加 */}
{i === 1 && (
<Stack align="center">
<Badge
//position="absolute" 👈コメントアウトすれば真ん中にBadgeが来る
top={2}
right={2}
px={3}
py={1}
color="white"
fontWeight="bold"
borderRadius="full"
fontSize="0.8rem"
backgroundImage="linear-gradient(to right, #ff9a36, #ff6b00)" // グラデーション
boxShadow="0 0 6px rgba(255, 140, 0, 0.4)"
>
<FaStar color="#ff6b00" />人気No.1
</Badge>
</Stack>
)}
<Card.Body>
<VStack align="center">
<Heading size="md">{card.title}</Heading>
<Text fontSize="sm" color="gray.600">
{card.subtitle}
</Text>
<Text fontWeight="bold" fontSize="lg">
{card.price}
</Text>
<Text>{card.plan}</Text>
{/* features */}
<Box mt={2}>
{card.features.map((feat, idx) => (
<HStack key={idx} align="center">
{feat.icon}
<Text fontSize="sm">{feat.text}</Text>
</HStack>
))}
</Box>
{/* ボタン */}
<motion.div whileHover={{ scale: 1.05 }} transition={{ duration: 0.3 }}>
{i === 1 ? (
<Button
backgroundImage="linear-gradient(to right, #ff9a36, #ff6b00)"
color="white"
_hover={{
backgroundImage: 'linear-gradient(to right, #ffa94d, #ff7800)',
}}
rounded="md"
mt={3}
>
<FaCrown />
お申し込み
</Button>
) : (
<Button
bg="black"
color="white"
_hover={{ bg: 'gray.700' }}
rounded="md"
mt={3}
>
お申し込み
</Button>
)}
</motion.div>
</VStack>
</Card.Body>
</Card.Root>
</motion.div>
</FadeIn>
))}
</SimpleGrid>
</FadeInStagger>
</Box>
);
};
以降のファイルに変更はないです。⇩
fadeinWithScroll.tsx
import { motion, useReducedMotion } from 'framer-motion';
import React, { createContext } from 'react';
import type { ComponentPropsWithRef } from 'react';
// 連動させてFadeInするかどうかのコンテキスト
const StaggerContext = createContext(false);
// スクロールに合わせて表示するためのViewportの設定
const viewport = { once: true, margin: '0px 0px -120px' };
// FadeInコンポーネント
export function FadeIn(props: ComponentPropsWithRef<typeof motion.div>) {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
{...props}
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 24 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, ease: 'easeOut' }}
viewport={viewport}
/>
);
}
// 複数要素を連動してFadeInさせたいときに使うProvider
export function FadeInStagger({ children }: { children: React.ReactNode }) {
return (
<StaggerContext.Provider value={true}>
<motion.div
initial="hidden"
whileInView="visible"
viewport={viewport}
variants={{
hidden: {},
visible: {
transition: {
staggerChildren: 0.2,
},
},
}}
>
{children}
</motion.div>
</StaggerContext.Provider>
);
}
App.tsx
// App.tsx
import React from 'react';
import { Box, ChakraProvider, Text, defaultSystem, Heading } from '@chakra-ui/react';
import { MenuHeader } from './framerMotionComponents/Header';
import { CardList } from './chakraComponents/ui/CardList';
function Section({ id, bg, children }: { id: string; bg: string; children: React.ReactNode }) {
return (
<Box
id={id}
height="100vh"
bg={bg}
display="flex"
alignItems="center"
justifyContent="center"
scrollMarginTop="80px"
>
<Text fontSize="4xl" fontWeight="bold" color="white">
{children}
</Text>
</Box>
);
}
export default function App() {
return (
<ChakraProvider value={defaultSystem}>
<MenuHeader />
<Box mt="80px">
<Section id="home" bg="blue.400">
Home Section
</Section>
<Section id="about" bg="teal.400">
About Section
</Section>
<Section id="services" bg="purple.400">
Services Section
<Box textAlign="center" py={10}>
<Heading mb={8}>Scroll Down to See Fade-In Cards ✨</Heading>
<CardList />
</Box>
</Section>
<Section id="contact" bg="pink.400">
Contact Section
</Section>
</Box>
</ChakraProvider>
);
}
main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { ChakraProvider, defaultSystem } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
const queryClient = new QueryClient();
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ChakraProvider value={defaultSystem}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</ChakraProvider>
</React.StrictMode>,
);
サイト
