前回の記事ではアイコンをベタ書きしていましたが、今回はReact-iconを使って表示してみます。
ディレクトリ構成
└── ReactAppProjekuto/
└── src/
├── chakraComponents/
│ └── ui/
│ └── CardList.tsx
├── framerMotionComponents/
│ └── fadeinWithScroll.tsx
├── App.tsx
└── main.tsx
実装
ポイント
FeatureとCardItemの型をしっかりつけることを意識して作りました。
これで features 内のアイコンとテキストにも安全に型が付き、
補完も効くようになります。
CradList.tsx
//(前略)
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: '優先サポート' },
],
},
];
全体のCardList.tsxはこちら⇩
CardList.tsx
import { Box, SimpleGrid, Heading, Text, Card, Button, VStack,Stack } 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: '優先サポート' },
],
},
];
export const CardList = () => {
return (
<Box p={6}>
<FadeInStagger>
<SimpleGrid columns={[1, 2, 2, 4]}>
{cards.map((card, i) => (
<FadeIn key={i}>
{/* motion.div でラップして、カーソルオーバー時の拡大アニメーション */}
<motion.div whileHover={{ scale: 1.1 }} transition={{ duration: 0.3 }}>
<Card.Root
boxShadow="md"
borderWidth="1px"
borderRadius="lg"
overflow="hidden"
mr="4"
p={4}
bg="white"
_hover={{ boxShadow: 'xl' }}
>
<Card.Body
display="flex"
justifyContent="center"
alignItems="center"
h="420px"
textOrientation="upright"
fontFamily="Noto Sans JP, sans-serif"
letterSpacing="0.1em"
>
<VStack align="center">
<VStack align="center">
<Heading size="md" mb={2}>
{card.title}
</Heading>
<Text fontSize="lg">{card.subtitle}</Text>
<Text fontWeight="bold" fontSize="lg">
{card.price}
</Text>
<Text fontSize="lg">{card.plan}</Text>
</VStack>
<VStack align="start">
{card.features.map((feature, index) => (
<Text key={index} fontSize="sm">
<Stack direction="row">
{feature.icon}
{feature.text}
</Stack>
</Text>
))}
</VStack>
</VStack>
{/* ボタン部分 */}
<motion.div whileHover={{ scale: 1.05 }} transition={{ duration: 0.3 }}>
{i === 1 ? (
// 2番目のカード専用ボタン
<Button
backgroundImage="linear-gradient(to right, #ff9a36, #ff6b00)"
color="white"
rounded="md"
>
<FaCrown />
お申し込み
</Button>
) : (
<Button bg="black" color="white" _hover={{ bg: 'gray.700' }} rounded="md">
お申し込み
</Button>
)}
</motion.div>
</Card.Body>
</Card.Root>
</motion.div>
</FadeIn>
))}
</SimpleGrid>
</FadeInStagger>
</Box>
);
};
fadeinWithScroll.tsxのコード⇩
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
// 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のコード
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>,
);
サイト
