🙄

# React × Tailwind × Zustand でエンジニアポートフォリオを作成してみた

2025/03/13に公開

はじめに

エンジニアとして、自分のスキルや実績をアピールできるポートフォリオサイトを作成しました。今回の開発では、React × Tailwind CSS × Zustand を活用し、レスポンシブ対応・アニメーション付きのサイトを構築しました。本記事では、実際の開発プロセスや技術選定のポイント、工夫した点について解説します。


技術スタック

今回のポートフォリオサイトの主な技術スタックは以下の通りです。

技術 用途
React (Vite) フロントエンドの構築
Tailwind CSS スタイリング
Zustand グローバル状態管理
Framer Motion アニメーション実装
TypeScript 型安全性の向上

1. プロジェクトのセットアップ

1.1 Vite + React + TypeScript のセットアップ

まず、Vite を使って React + TypeScript のプロジェクトを作成します。

npm create vite@latest my-portfolio --template react-ts
cd my-portfolio
npm install

1.2 必要なパッケージのインストール

npm install tailwindcss framer-motion zustand lucide-react
npx tailwindcss init -p

Tailwind CSS の設定ファイル tailwind.config.js を開き、以下のように修正します。

module.exports = {
  content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
  theme: {
    extend: {},
  },
  plugins: [],
};

index.css に Tailwind のベーススタイルを適用します。

@tailwind base;
@tailwind components;
@tailwind utilities;

2. ナビゲーションバーの作成

2.1 Zustand でメニューの開閉を管理

src/store/useMenuStore.ts に Zustand を使った状態管理を実装します。

import create from "zustand";

interface MenuState {
  isOpen: boolean;
  toggleMenu: () => void;
  closeMenu: () => void;
}

const useMenuStore = create<MenuState>((set) => ({
  isOpen: false,
  toggleMenu: () => set((state) => ({ isOpen: !state.isOpen })),
  closeMenu: () => set({ isOpen: false }),
}));

export default useMenuStore;

2.2 ナビゲーションバーの実装

ナビゲーションバー (src/components/Navbar.tsx) では、ハンバーガーメニューが開閉するアニメーションを Framer Motion で追加しています。

import { motion, AnimatePresence } from "framer-motion";
import { Menu, X } from "lucide-react";
import useMenuStore from "../store/useMenuStore";

const Navbar = ({ scrollToSection, activeSection }) => {
  const { isOpen, toggleMenu, closeMenu } = useMenuStore();

  return (
    <nav className="fixed top-0 left-0 w-full bg-white/80 backdrop-blur-sm border-b z-50">
      <div className="max-w-6xl mx-auto px-4 flex items-center justify-between h-16 w-full">
        <h1 className="text-lg font-bold">My Portfolio</h1>

        <motion.button onClick={toggleMenu} className="md:hidden">
          {isOpen ? <X size={32} /> : <Menu size={32} />}
        </motion.button>
      </div>

      <AnimatePresence>
        {isOpen && (
          <motion.div
            className="fixed top-0 right-0 w-3/4 h-screen bg-black bg-opacity-80 flex flex-col items-center justify-center"
            initial={{ x: "100%", opacity: 0 }}
            animate={{ x: 0, opacity: 1 }}
            exit={{ x: "100%", opacity: 0 }}
            transition={{ duration: 0.4, ease: "easeInOut" }}
          >
            <ul className="text-white text-xl space-y-4">
              {["home", "profile", "skill", "portfolio", "contact"].map((item) => (
                <li key={item}>
                  <button onClick={() => { scrollToSection(item); closeMenu(); }}>
                    {item.toUpperCase()}
                  </button>
                </li>
              ))}
            </ul>
          </motion.div>
        )}
      </AnimatePresence>
    </nav>
  );
};

export default Navbar;

3. ポートフォリオ一覧の実装

ポートフォリオ一覧を src/components/Portfolio.tsx に実装しました。

const portfolioItems = [
  { title: "Project 1", description: "プロジェクト1の説明", image: "/assets/images/project1.jpg", link: "#" },
  { title: "Project 2", description: "プロジェクト2の説明", image: "/assets/images/project2.jpg", link: "#" },
];

const Portfolio = () => {
  return (
    <section id="portfolio" className="py-20">
      <div className="max-w-6xl mx-auto px-4">
        <h2 className="text-3xl font-bold text-center mb-4">PORTFOLIO</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
          {portfolioItems.map((item, index) => (
            <a key={index} href={item.link} className="group relative block bg-white rounded-lg shadow-lg overflow-hidden">
              <img src={item.image} alt={item.title} className="w-full h-48 object-cover" />
              <div className="p-6">
                <h3 className="text-xl font-bold mb-2">{item.title}</h3>
                <p className="text-sm text-gray-600">{item.description}</p>
              </div>
            </a>
          ))}
        </div>
      </div>
    </section>
  );
};

export default Portfolio;

まとめ

今回の開発では、React × Tailwind CSS × Zustand × Framer Motion を活用し、スタイリッシュで使いやすいポートフォリオサイトを構築しました。カスタマイズのしやすさを重視し、管理画面の導入など今後の拡張性も考慮しています。

備忘録として残しておきます。

Discussion