はじめに
Next.jsを使用して、インタラクティブなアドベントカレンダーUIを実装してみました。
3Dアニメーションや雪のエフェクトを取り入れつつ、TypeScriptの型安全性も確保しています。
完成イメージ
- 24個のドアが7日ごとにグリッド表示
- ドアをクリックすると3Dアニメーションで開く
- 雪が降るエフェクト
- 中にランダムな絵文字とメッセージ
https://adbent-yusukekoides-projects.vercel.app/
実装で工夫した点
1. ハイドレーションエラーの解決
App Routerでの最大の課題は、サーバーサイドレンダリング(SSR)とクライアントサイドレンダリング(CSR)の整合性でした。
// クライアントサイドのみの処理を制御
const [isClient, setIsClient] = useState(false);
useEffect(() => {
setIsClient(true);
}, []);
// ランダムな要素はクライアントサイドでのみ実行
const [currentSurprises] = useState(() => {
const initialSurprises: SurpriseMap = {};
if (typeof window !== 'undefined') {
const shuffled = [...surprises].sort(() => Math.random() - 0.5);
// ...
}
return initialSurprises;
});
2. TypeScriptの型安全性確保
オブジェクトのインデックスアクセスに関する型エラーに悩まされました。
インターフェースを定義することで解決しました:
interface Surprise {
emoji: string;
message: string;
}
interface SurpriseMap {
[key: number]: Surprise;
}
const [currentSurprises] = useState<SurpriseMap>(() => {
// ...
});
3. 3Dアニメーションの実装
CSSの3D変換を使用して、本物のドアのような開閉アニメーションを実現:
.door {
transform-style: preserve-3d;
transition: transform 0.5s;
}
.door.open {
transform: rotateY(-105deg);
}
4. カレンダーグリッドの実装
7日ごとの改行を実現するために、配列を週単位で分割:
const renderCalendarGrid = () => {
const weeks = [];
let currentWeek = [];
for (let i = 1; i <= 24; i++) {
currentWeek.push(<Door key={i} number={i} />);
if (currentWeek.length === 7 || i === 24) {
weeks.push(
<div key={weeks.length} className="flex justify-center w-full">
{currentWeek}
</div>
);
currentWeek = [];
}
}
return weeks;
};
5. 雪のエフェクト
複数の雪の結晶文字を使い、ランダムな動きを実現:
const snowflakes = ["❄", "❅", "❆", "✻", "✺", "*"];
const getSnowflakes = () => {
return Array.from({ length: 8 }).map((_, i) => ({
flake: snowflakes[Math.floor(Math.random() * snowflakes.length)],
delay: Math.random() * 5,
duration: 2 + Math.random() * 3,
position: Math.random() * 100
}));
};
困った点と解決策
1. SSRとCSRの不一致
問題:
-
Math.random()
を使用した要素の生成 - クライアントサイドでのみ必要なアニメーション
解決策:
-
isClient
フラグでレンダリングを制御 -
useEffect
でクライアントサイド処理を分離
2. TypeScriptのエラー
問題:
- インデックスシグネチャの欠落
- 暗黙的なany型
解決策:
- 適切なインターフェースの定義
- 型アノテーションの追加
3. モバイル対応
問題:
- 小さい画面でのグリッドのはみ出し
- タッチデバイスでの3Dエフェクト
解決策:
- レスポンシブなグリッドレイアウト
-
hover
状態の適切な処理
<div className="max-w-7xl mx-auto p-4 sm:p-6">
<div className="flex flex-col gap-4">
{/* グリッドコンテンツ */}
</div>
</div>
デプロイ方法
Vercelを使用して簡単にデプロイできます:
- GitHubにプッシュ
- Vercelでプロジェクトをインポート
- 自動デプロイの設定
# デプロイコマンド
vercel
まとめ
Next.js を使用したインタラクティブUIの実装は、
いくつかの課題がありましたが、適切な設計と型定義により解決できました。
特に以下の点が重要でした:
- SSR/CSRの適切な使い分け
- TypeScriptの型安全性確保
- パフォーマンスとアニメーションの両立