0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【React(V19系)】ChakraUI(V3系)とMotion(旧 Framer-motion)を使ってCardに内容を追加してみる(React-Icon使用)

Last updated at Posted at 2025-11-12

前回の記事ではアイコンをベタ書きしていましたが、今回はReact-iconを使って表示してみます。

image.png

ディレクトリ構成

└── ReactAppProjekuto/
    └── src/
        ├── chakraComponents/
        │   └── ui/
        │       └── CardList.tsx
        ├── framerMotionComponents/
        │   └── fadeinWithScroll.tsx
        ├── App.tsx
        └── main.tsx

実装

ポイント

FeatureCardItemの型をしっかりつけることを意識して作りました。
これで 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>,
);

サイト

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?