【React + TypeScript】文系出身者がこだわった、なめらかに動くポートフォリオサイトの作り方
こんにちは!文系出身でプログラミング歴2年の駆け出しエンジニアです。この記事では、自分の技術力のアウトプットとして作成したポートフォリオサイトについて、特にこだわったデザインや滑らかなアニメーションの実装方法を中心に解説します。
はじめに:なぜポートフォリオサイトを作ったのか
法学部出身の私がプログラミングを始めたのは2年前。独学と仕事を通じて技術を身につけてきましたが、「自分の技術力を一度きちんとアウトプットしたい」という思いが強くなり、ポートフォリオサイト制作に挑戦しました。
特に重視したのは以下の点です:
- モダンな技術スタックの採用
- ユーザーが心地よいと感じるスムーズな動き
- レスポンシブ対応
- 直感的なUI/UX
技術スタック
- フロントエンド
- React 19.0.0
- TypeScript 5.7.2
- Vite 6.2.1
- スタイリング
- CSS Variables(カスタムプロパティ)
- デプロイ
- Firebase Hosting
サイト構成
基本的なセクション構成は以下の通りです:
- ヒーローセクション
- 自己紹介セクション
- スキルセクション
- プロジェクトセクション
- コンタクトセクション
- フッター
こだわり①:CSS変数を活用した一貫したデザインシステム
デザインの一貫性を保つために、CSS変数(カスタムプロパティ)を活用しました。これにより、色やサイズ、アニメーション速度などを簡単に統一でき、変更も容易になります。
:root {
/* メインカラーパレット */
--primary-color: #5e60ce;
--primary-light: #7a7de3;
--primary-dark: #4c4da3;
--secondary-color: #64dfdf;
--accent-color: #ff7eee;
/* テキストカラー */
--text-primary: #2b2c34;
--text-secondary: #555;
--text-light: #777;
--text-on-dark: #f8f9fa;
/* 背景カラー */
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #edf2f7;
--bg-dark: #2b2c34;
/* トランジション */
--transition-fast: all 0.2s ease;
--transition-normal: all 0.3s ease;
--transition-slow: all 0.5s ease;
/* 他にもスペーシングやボーダーラディウスなど */
}
これらの変数を使うことで、例えばボタンのスタイルを以下のように簡潔に定義できます:
.btn {
padding: var(--space-3) var(--space-6);
border-radius: var(--border-radius-full);
transition: var(--transition-normal);
/* 他のスタイル */
}
こだわり②:スムーズなスクロールと遷移アニメーション
ユーザーエクスペリエンスで最も重視したのが、サイト内の動きの滑らかさです。
スムーズスクロール
HTML全体にスムーズスクロールを適用:
html {
scroll-behavior: smooth;
}
アニメーションのタイミング調整
特に気をつけたのは、アニメーションのタイミングとイージング(加速・減速)の調整です。例えば、ヒーローセクションの要素をフェードインさせる際には:
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.hero-text {
animation: fadeIn 0.8s ease forwards;
}
.hero-image-container {
animation: fadeIn 1s ease 0.3s forwards;
opacity: 0;
}
ここでポイントなのは、テキストを先に表示し(0.8秒)、少し遅れて(0.3秒後)画像を表示する演出です。こうすることで、ユーザーの視線の流れをコントロールし、自然な印象を与えることができます。
浮動するカードエフェクト
ヒーローセクションでは「経験年数」や「プロジェクト数」を示すカードが浮遊するアニメーションを実装しました:
.floating-card {
position: absolute;
display: flex;
padding: var(--space-3) var(--space-4);
border-radius: var(--border-radius-md);
background-color: var(--bg-primary);
box-shadow: var(--box-shadow-md);
z-index: 2;
}
.card-experience {
top: 10%;
left: -15%;
animation: floatCard 3s ease-in-out infinite;
}
.card-projects {
bottom: 15%;
right: -10%;
animation: floatCard 3s ease-in-out 1.5s infinite;
}
@keyframes floatCard {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
このように時間差を付けて浮遊させることで、自然で有機的な動きを演出しています。
こだわり③:インタラクティブな要素
フィルタリング機能
プロジェクトセクションでは、カテゴリ別にフィルタリングできる機能を実装しました:
// Projects.tsxの一部抜粋
const [filter, setFilter] = useState<FilterType>('all');
const filteredProjects = filter === 'all'
? projects
: projects.filter(project => project.category === filter);
// フィルターボタン
<div className="filter-buttons">
<button
className={`filter-btn ${filter === 'all' ? 'active' : ''}`}
onClick={() => setFilter('all')}
>
すべて
<span className="filter-count">{projects.length}</span>
</button>
{/* 他のフィルターボタン */}
</div>
// フィルター適用後のプロジェクト表示
<div className="projects-grid">
{filteredProjects.map(project => (
<div className="project-card" key={project.id}>
{/* プロジェクトカードの内容 */}
</div>
))}
</div>
モーダル表示
プロジェクトカードをクリックすると詳細がモーダルで表示される機能も実装しました:
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
const [modalOpen, setModalOpen] = useState<boolean>(false);
const handleProjectClick = (project: Project) => {
setSelectedProject(project);
setModalOpen(true);
document.body.style.overflow = 'hidden'; // スクロール防止
};
const closeModal = () => {
setModalOpen(false);
document.body.style.overflow = 'auto'; // スクロール復活
};
モーダルのアニメーションもこだわりました:
.project-modal {
animation: modalFadeIn 0.3s ease;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
こだわり④:レスポンシブデザイン
様々な画面サイズに対応するため、メディアクエリを活用しました:
/* 基本レイアウト */
.hero-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--space-12);
}
/* タブレットサイズ */
@media screen and (max-width: 992px) {
.hero-content {
grid-template-columns: 1fr;
text-align: center;
}
.hero-image-container {
order: 1; /* 画像を上に */
}
.hero-text {
order: 2; /* テキストを下に */
}
}
/* スマートフォンサイズ */
@media screen and (max-width: 576px) {
html {
font-size: 12px; /* 基本フォントサイズを小さく */
}
.section-title {
font-size: 1.8rem;
}
/* 他の調整 */
}
苦労した点と学び
State管理の最適化
特にプロジェクトセクションでは、フィルタリングとモーダル表示の状態管理を組み合わせる必要があり、初めは複雑になってしまいました。ですが、適切にステートを分割し、関数をシンプルにすることで可読性を向上させました。
CSSアニメーションのタイミング調整
複数の要素が連動するアニメーションのタイミング調整は、思った以上に時間がかかりました。デバイスによって表示が異なる場合もあり、様々な環境でテストする必要がありました。
フォームのバリデーション
コンタクトフォームのバリデーションとユーザー体験の両立に苦労しました。エラーメッセージの表示タイミングや、フォーカス時の挙動など、細かい部分まで調整しています:
// フォーム入力項目のフォーカス状態管理
const [isFocused, setIsFocused] = useState({
name: false,
email: false,
subject: false,
message: false
});
const handleFocus = (field) => {
setIsFocused(prev => ({
...prev,
[field]: true
}));
};
const handleBlur = (field) => {
if (!formData[field]) {
setIsFocused(prev => ({
...prev,
[field]: false
}));
}
};
今後の改善点
現在も継続的に改善を行っていますが、主な今後の課題は:
- さらなるプロジェクト追加(現在は一部仮データ)
- ダークモード実装
- 多言語対応
- パフォーマンス最適化
まとめ
文系出身のエンジニアとして、デザインと技術の両面からポートフォリオサイトを作り上げる過程は非常に勉強になりました。特にCSSアニメーションと状態管理の組み合わせは、見た目だけでなくユーザー体験という観点からも深く考えるきっかけになりました。
何か質問やフィードバックがあれば、コメントいただけると嬉しいです!