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に内容とバッジ(Badge)を追加してみる(React-Icon使用)

Posted at

前回の記事に追加してBadgeを追加してみます。

完成イメージ

image.png

ディレクトリ構成

└── 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>,
);

サイト

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?