🎠 ライブラリなしで無限カルーセルをCursorで作ってみた話(Next.js + React)
🎠 ライブラリなしで無限カルーセルをCursorで作ってみた話(Next.js + React)
Swiperを使えばすぐできる。でも「使わずに作ったら何が大変なのか?」を、AIペアプログラミングツール「Cursor」と一緒に試してみたら、学びが盛りだくさんだった。
1. この記事で伝えたいこと
- ライブラリなしで無限カルーセルを作るのは意外と骨が折れる!
- Next.js + React HooksでUIと状態管理を分離する設計の実践例
- 自作による細かいUX再現やパフォーマンス配慮のリアル
- Cursor(AIペアプログラミング)を活用した開発プロセスの実践例
- 「使う前に作ってみる」ことで、ライブラリのありがたさも分かる
2. Cursorを使って開発した背景
本記事は、AIペアプログラミングツール「Cursor」を活用しながら、ライブラリなしで無限カルーセルを自作した開発体験をまとめたものです。
Cursorを使うことで、設計や実装の壁打ち・リファクタ・技術調査・ドキュメント作成まで、AIと一緒に進める新しい開発スタイルを体験できました。
「AIと一緒に作ると、どこが楽で、どこが難しいのか?」という視点も交えて、実装の工夫や学びを紹介します。
3. 作ったもの:ライブラリゼロで実現した“学べるカルーセル”
- 💡 外部UIライブラリを一切使わず、Next.js + React だけで構築した「無限カルーセル」コンポーネント
- 🎠 Swiper風のUX(スワイプ / オートスクロール / 無限ループ)を自力で再現
- 🖌️ Tailwind CSS活用による柔軟で直感的なデザイン調整
- 🎴 9種類の絵文字カードをランダム生成・両面flip付きでスライド表示
※実際の描画は仮想的に30枚相当をループ再配置 - 🧠 状態管理・アニメーション・レスポンシブ対応などReact HooksとNext.js設計の学びが詰まった実践作
- 📱 タッチ&マウス両対応、モバイル/PCレスポンシブ切り替え
- 🔍 ソースは完全オープンソース、技術ブログと連携して構造も解説中
🌐 実際に動かしてみたい方はこちらから👇
4. ソースコードはこちら(GitHub)
本プロジェクトはGitHubで公開しています。Next.js(App Router構成)・React 19・TypeScript・Tailwind CSSを組み合わせたモダン構成です。
🔗 zabuton-100/next-infinite-carousel
💬 気になる点や改善アイデアがあれば、Issue・PR・Zennコメントなどでお気軽にどうぞ!
5. 技術スタック
- Next.js 15 (App Router)
- React 19 + TypeScript
- Tailwind CSS
- emoji-dictionary / unicode-emoji-json(絵文字名・カテゴリ取得)
※外部のUIライブラリ(Swiper等)は未使用
6. 実装のポイントと工夫
6-1. スケルトンUIによるUX向上
- 初期表示時、PCとスマートフォン(SP)それぞれの画面幅に応じて最適なスケルトンUI(ローディング用プレースホルダー)を表示。
- 画像やデータの取得前でもレイアウト崩れや“空白”を感じさせず、ユーザー体験を損なわない工夫。
- PC/SPでスケルトンの枚数やサイズも自動調整されるため、どのデバイスでも自然なローディング体験を実現。
6-2. 無限スクロールの実現方法
- 表示+先読み用の絵文字配列(9個)を常に更新。
- 「中央(30枚中の10番目)に戻すジャンプ」を挟むことで見た目上の無限ループを再現。
// 例: 配列の端に来たらアニメーションを一時無効化して中央にジャンプ
function slideTo(index: number) {
if (index < threshold) {
jumpToCenter();
} else {
setCurrentIndex(index);
}
}
疑似的に「無限に見える」ようにするのが難しい!
詳細解説:なぜ「中央に戻すジャンプ」で無限ループになるのか?
- 通常のカルーセルは配列の端(最初や最後)に到達すると、それ以上スライドできなくなる。
- 無限ループを実現するため、表示用の配列を工夫。
- 例:実際の画像が5枚(A, B, C, D, E)ある場合、これを「A, B, C, D, E, A, B, C, D, E, ...」のように繰り返して並べる。
- 中央付近(例えば10番目)を初期表示位置にする。
- 端に来たときはアニメーションを一時的に無効化して中央にジャンプ。
- これにより、ユーザーからは「無限にスライドできる」ように見える。
具体例
例えば、配列が以下のようになっているとします(A〜Eが実際の画像):
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
A | B | C | D | E | A | B | C | D | E | A | B | C | D | E |
- 初期表示は「10番目(A)」。
- 右にスライドしていくと、11(B)、12(C)、…と進みます。
- もし14(E)まで行ったら、アニメーションなしで10(A)にジャンプします。
- 逆に左にスライドして0(A)まで行ったら、アニメーションなしで中央(10番目)にジャンプします。
補足:この方式は汎用的なアルゴリズム
この「中央に戻すジャンプ」を使った無限スクロールの実装方法は、ランダムな絵文字に限らず、汎用的に使えるテクニックです。
- 画像カルーセル(バナーや商品画像など)
- テキストリスト(ニュースティッカーやコメント欄など)
- 任意のReact/Vue/JSX要素(カード、ボタン、カスタムコンポーネントなど)
配列の内容が「循環」しても問題ないUIであれば、どんな要素にも応用できます。
- 元データを複製して長い配列を作る
- 初期表示を中央付近にする
- 端に来たら中央にジャンプする
- スクロールやスライドのたびに「今どの要素を表示しているか」を元データのインデックスに変換する
この方式は、カルーセルやリストなど「循環UI」を作りたいときの定番テクニックです。
6-3. スワイプ&ドラッグ対応
- マウス/タッチの座標差分でスライド方向を判定。
-
useRef
でDOMへのアクセスとdrag状態を管理。 - スライド距離が閾値を超えたら移動、未満ならスナップバック。
- PointerEventの取り扱いやイベントキャンセルが地味に面倒。
- ドラッグイベント(onMouseDown/onTouchStart など)を正しくリッスンしないと、スワイプ操作自体が反応しない。
- UIの直感的な操作性を担保するには、ユーザーの入力イベントを網羅的にハンドリングすることが重要。
6-4. flipアニメーションの工夫
- 表→裏→表と切り替えるアニメーションを
flipInX
で表現。 - インデックス単位で反転中かどうかをstateで管理。
const [flippedIndexes, setFlippedIndexes] = useState<Set<number>>(new Set());
- スライド中にだけ発火&終わったら戻す、という状態制御が重要。
- flipInXアニメーションの実装は、Owl Carouselのアニメーションデモを参考にしている。
6-5. オートスクロールの一時停止ロジック
- 初期状態では1.5秒ごとに自動スライド。
- ユーザーが操作すると即座にオートスクロールを止める。
- 一度止めたら以降は自動再開しない。
- こういう「人間らしい挙動」の再現も、自作だと全部書く必要あり!
6-6. UI最適化とレスポンシブ対応
- モバイル・PCで表示枚数を切り替え(
window.innerWidth
、現状はどちらも3枚表示)。 -
useEffect
でリサイズ時に再計算。 - TailwindのユーティリティでCSSも柔軟に変更。
7. 学びポイント(React / Next.js)
7-1. useRef × useEffect の地味だけど重要な使い方
- ドラッグ中のイベント管理は
ref
でDOMとフラグを保持。 - 状態更新は遅延するため、リアルタイムな制御はref必須。
7-2. サーバー/クライアントコンポーネントの責務分離
- サーバー側:初期データ(絵文字)を生成。
- クライアント側:UI描画&ユーザー操作。
// page.tsx
import CarouselServer from "@/components/CarouselServer";
export default function Page() {
return <CarouselServer />;
}
// CarouselServer.tsx(Server Component)
return <InfiniteCarousel emojiPairsArray={arr} />
- Next.jsの設計思想に合った、責務分離の実践例!
7-3. requestAnimationFrameの活用
- 秒ズレが起きやすい
setInterval
の代わりにrequestAnimationFrame
を使って「高精度な現在時刻表示」を実現。
useEffect(() => {
const updateTime = () => {
setDate(new Date());
requestAnimationFrame(updateTime);
};
updateTime();
return () => cancelAnimationFrame(updateTime);
}, []);
-
requestAnimationFrame
により毎フレーム(約16msごと)で現在時刻を監視。 - Reactの再レンダリングは状態が変化したときのみ発生するため、秒が切り替わったタイミングでのみ画面が再描画される。
- setIntervalと異なり、ブラウザの描画タイミングと同期するため、より滑らかで正確な秒更新が可能。
8. ライブラリを使わなかったからこそ分かったこと
- 無限スクロールやflipアニメーションの裏側を理解できた。
- 状態管理の難しさ(同期 vs 非同期、ref vs state)。
- UIのちょっとした気遣いも実装コストがかかる。
- Next.jsのサーバー/クライアント分離の恩恵を実感。
9. 読者レベル別:この実装で得られる学びポイント
9-1. Next.js 初学者向け
-
App RouterとServer Componentsの分離が自然に理解できた
- サーバー側でデータ(絵文字配列)を生成し、クライアント側でUIやユーザー操作を担当する構造を実装することで、Next.jsの設計思想(App Router/Server Componentsの責務分離)が体感できます。
9-2. React中級者向け
-
useRef vs useState の使い分け実感した話
- ドラッグ中の座標や一時的なフラグ管理にはuseRef、UIの状態やアニメーション制御にはuseStateを使い分けることで、パフォーマンスやリアルタイム性を両立できる実感が得られます。
9-3. チーム開発向け
-
責務分離の思想がそのまま構造に現れる
- サーバー/クライアント、UI/ロジック、状態/副作用など、役割ごとにファイルやHooksを分離することで、チームでの分担や保守性が高まる設計になっています。Next.jsの思想を活かしたチーム開発の参考にもなります。
10. よくある落とし穴/詰まりポイント紹介
実装中に「これ、地味にハマる!」と感じたポイントをまとめます。
-
PointerEvent の初期化漏れでスワイプが動かない
- onMouseDown/onTouchStartなどのイベントリスナーを正しく付け忘れると、どんなにロジックが正しくてもスワイプが一切反応しません。イベントのバインド漏れは要注意です。
-
useRef の初期値 null が原因で scrollIntoView が落ちる
- ref.currentがnullのままscrollIntoView()を呼ぶとエラーになります。useRefでDOM参照を使う場合は、nullチェックやマウントタイミングに注意しましょう。
-
transition と translateX の相性問題でアニメがバグる
- CSSのtransitionとJSでのtranslateX操作を組み合わせると、アニメーションが意図せずカクついたり、瞬間移動したりすることがあります。アニメーションを一時的に無効化(transition: none)するタイミングや、状態の切り替え順序に気をつける必要があります。
こうした「地味だけど詰まりやすい」ポイントも、AIペアプログラミング(Cursor)で壁打ちしながら一つずつ解消していきました。
11. この実装で使っているReact Hooksとその理由
本プロジェクトでは、Reactの標準HooksやカスタムHooksを積極的に活用しています。
それぞれの用途と理由、そしてカルーセルのどの部分で使っているかは以下の通りです。
11-1. useState
-
currentIndex
:現在表示中のスライド位置を管理 -
isAnimating
/isAnimatingAll
:アニメーション中かどうかのフラグ -
noTransition
:アニメーションを一時的に無効化するためのフラグ -
itemWidth
/translateX
:スライドの幅や位置の管理 -
flippedIndexes
/flippingBackIndexes
:flipアニメーション中のカードインデックス管理 -
isAutoScrollStopped
:オートスクロールの一時停止状態 -
showCheck
:チェックアイコンの一時表示制御 -
lastScrollDirection
:直近のスクロール方向(左右・ボタン/スワイプ)
11-2. useRef
-
carouselRef
/itemRef
:カルーセル本体や各カードのDOM参照 -
isFirstRender
:初回描画かどうかの判定用フラグ -
autoScrollIntervalRef
/checkTimerRef
:setInterval/setTimeoutのID管理 -
dragState
:ドラッグ中の座標・状態(isDragging, startX, lastX, isTouch, startTranslateX など)
11-3. useEffect
- スライド幅やウィンドウリサイズ時の再計算(
itemWidth
の更新) - 初期位置セットや中央ジャンプ時のアニメーション制御
- 表示中の絵文字やタイトルの動的変更
- オートスクロールの開始・停止、クリーンアップ
- flipアニメーションのタイミング制御や、visibleなカードのログ出力
11-4. useCallback
-
slideTo
:スライド移動処理(アニメーション・flip制御含む) -
stopAutoScroll
:ユーザー操作時の自動スクロール停止 -
handleAutoScrollNext
:自動で次スライドに進める処理 -
triggerCheck
:チェックアイコンの一時表示処理
11-5. カスタムHooks: useResponsiveCarouselCount
- 画面幅に応じて「表示枚数(visibleCount)」や「モバイル判定(isMobile)」を返すロジックをカスタムHooks化。
- カルーセル本体(InfiniteCarousel)で呼び出し、SP/PCでのUI切り替えやスライド数の自動調整に利用。
- このHooksを使うことで、レスポンシブ判定のロジックを他のコンポーネントでも再利用可能に。
12. Next.js・React初学者向け:実装のポイントと読み解きガイド
12-1. 🖼️ スワイプ時の矢印画像表示の実装ポイント
- スワイプやボタン操作をしたときに一瞬だけ表示される矢印アイコン(SVG)は、Reactの「状態管理(useState)」と「タイマー(setTimeout)」を組み合わせて実現。
-
showCheck
という状態をtrueにすると矢印が表示され、0.7秒後に自動で非表示。 - スワイプやボタンの方向に応じて左右の矢印を切り替えているので、「どちらに動いたか」が直感的に分かるUI。
- こうした一時的なフィードバックは、ユーザー体験を向上させる大事な工夫。
12-2. 🎨 Tailwind CSSを使った実装ポイント
- このプロジェクトでは、CSSを直接書く代わりに「Tailwind CSS」というユーティリティファーストなCSSフレームワークを使用。
- たとえば、
className="flex items-center justify-center w-[240px] h-80 ..."
のように、HTMLタグにたくさんのクラスを並べるだけで、レイアウトや色、余白、レスポンシブ対応、アニメーションまで細かく調整可能。 - CSSファイルをほとんど書かずに済むので、初学者でもUIの調整がしやすい。
12-3. 📖 InfiniteCarousel.tsxの読み解き方ガイド
このファイルは一見複雑ですが、次の順番で読み進めると理解しやすいです:
-
props(emojiPairsArray)の受け取りと型定義
- どんなデータを受け取っているか、型(EmojiPair)を確認しましょう。
-
カスタムHooksの呼び出し
-
useResponsiveCarouselCount
で画面幅ごとの表示枚数やモバイル判定を取得しています。
-
-
useState/useRefで管理している状態・参照の一覧
- どんなUI状態やDOM参照があるか、変数名と用途をざっと把握しましょう。
-
useEffect群の役割
- 初期化、リサイズ、タイトル変更、オートスクロール、アニメーション制御など、各副作用のタイミングと目的を確認します。
-
コールバック関数(useCallback)
-
slideTo
やstopAutoScroll
など、主要な操作ロジックの流れを追いましょう。
-
-
ドラッグ・スワイプ・ホイール等のイベントハンドラ
- ユーザー操作にどう反応するか、イベントごとの処理を確認します。
-
JSXの構造とTailwindクラス
- どのようにUIが組み立てられているか、各要素の役割やスタイリングを見てみましょう。
-
アニメーションやflipの制御ロジック
- flipInXや一時的なSVG表示など、動きのある部分の状態遷移を追うと理解が深まります。
この順番で「データ→状態→副作用→操作→UI構造→アニメーション」と段階的に読み解くことで、全体像と細部のつながりがスムーズに理解できます。
分からない部分は、まず「どんな状態(useState)やイベント(onClick, onTouchStartなど)があるか」を探してみるのがおすすめです。
13. Q&Aセクション:実装の意図を自問自答形式で
読者が「なぜこう書いたのか?」と疑問に思いそうなポイントをQ&A形式でまとめました。
Q. なぜ useRef
をこんなに使っているの?
- 実は
useState
だけだと、ドラッグ中のリアルタイムな位置やタイミングが正しく扱えなかったため、refでフラグや座標を管理しています。 - useRefは再レンダリングを発生させずに値を保持できるので、パフォーマンスや操作感の面で有利です。
Q. なぜflipアニメーションの状態をSetで管理しているの?
- 複数のカードが同時にアニメーションする可能性があるため、インデックスごとに「今flip中かどうか」をSetで管理しています。
- これにより、個別にアニメーションのON/OFFや復帰タイミングを制御できます。
Q. なぜTailwind CSSを使っているの?
- 細かいレイアウトや配色、レスポンシブ対応、アニメーションなどを直感的かつ高速に調整できるからです。
- CSSファイルをほとんど書かずに済み、UIの試行錯誤がしやすくなります。
Q. なぜオートスクロールは一度止めたら再開しないの?
- ユーザーが操作した時点で「自分で動かしたい」という意図があると考え、以降は自動で動かさない設計にしています。
- 人間らしい挙動を重視しました。
Q. なぜpropsでemojiPairsArrayを渡しているの?
- サーバー側で初期データ(絵文字配列)を生成し、クライアント側でUIや操作を担当することで、Next.jsのServer Components/Client Componentsの責務分離を実践しています。
14. Cursor × AI開発 実践ガイドも公開中
本記事の実装や開発プロセスで得たAI活用の知見・ノウハウは、下記Zenn記事でも詳しくまとめています。
AIペアプログラミングやCursor活用に興味のある方はぜひご参照ください。
15. 🆕 新仕様: スワイプごとに新しい絵文字がランダム生成
- スワイプや自動スクロールで新しいカードが表示されるたび、既存と重複しない新しいランダムな絵文字がクライアント側で動的に生成・追加されるようになりました。
- これにより、無限にスワイプしても毎回異なる絵文字が現れ、同じ絵文字が繰り返し出現しません。
- 技術的には、
InfiniteCarousel.tsx
でuseState管理し、getUniqueRandomEmojiPair
関数で重複を避けて新規生成しています。
技術的な工夫
- クライアント側で
emojiPairsArray
を状態管理し、スワイプ時に新しいユニークな絵文字を都度生成・追加することで、体験の単調さを防いでいます。
16. 最後に
「ただのカルーセル」に見えて、実は状態管理・アニメーション・レスポンシブと、実践的なフロント技術が詰まっています。
「ライブラリなしで作ってみる」ことで、Reactの地力や設計の引き出しを増やす練習にもなるので、ぜひチャレンジしてみてください!
Discussion