カレンダー UI を自作した話 — ライブラリを使わない判断と flexbox 実装

2026.02.10
Share:

はじめに — カレンダーUIの誘惑

Web アプリでカレンダーを表示する必要が出てきたとき、まず頭に浮かぶのは「ライブラリを使おう」ではないでしょうか。FullCalendar、react-big-calendar、react-day-picker... 選択肢は豊富にあります。

Wakulier(フリーランス向け依頼管理ツール)でもカレンダー予約画面が必要でした。最初は当然ライブラリを使うつもりでしたが、4つの選択肢を検討した末に「完全自作」を選びました。

その判断プロセスと、flexbox で週間スケジュールビューを実装した方法を共有します。

背景 — Wakulier に必要だったカレンダー

Wakulier はフリーランスがクライアントからの依頼を管理するツールです。Google カレンダーと連携して空き時間を取得し、クライアントが予約できるスケジュールビューが必要でした。

要件を整理すると:

  • 週表示: 1週間の空き時間を一覧で表示
  • 30分スロット: 時間帯を30分単位で区切る
  • 4週間先まで: フリーランスは数週間先の予定も確認したい
  • レスポンシブ: モバイルでも使いやすいこと

最初はカレンダーピッカー(日付を選択するUI)で実装していましたが、「空き時間が視覚的にわかりにくい」という問題がありました。Google カレンダー風の週表示に変更することにしました。

4つの選択肢を検討する

2025年12月29日の開発日記には、こう記録しています:

選択肢:

  1. FullCalendarライブラリ → 機能過剰、バンドルサイズ大
  2. 自作 → 工数かかるが軽量
  3. react-big-calendarベース + カスタマイズ → 不採用(依存の問題)
  4. 完全自作 → 採用

それぞれの検討結果を詳しく振り返ります。

選択肢1: FullCalendar

FullCalendar は Web カレンダーライブラリの定番です。機能は申し分なく、ドラッグ&ドロップやリサイズまでサポートしています。

しかし、Wakulier で必要なのは「空き時間の表示」だけです。イベントの編集やドラッグ操作は不要でした。FullCalendar のバンドルサイズ(gzip で約 40KB〜)は、この用途にはオーバースペックでした。

選択肢2: 軽量な自作

「必要最小限で自作する」という選択肢です。ただ、この時点では工数の見積もりが不透明で、後回しにしました。

選択肢3: react-big-calendar

FullCalendar より軽量な react-big-calendar も候補に上がりました。しかし、moment.js への依存が気になりました。Wakulier では日付操作に date-fns を使っており、moment.js を追加で入れるとバンドルサイズが膨らみます。

date-fns アダプターもありますが、追加の設定が必要で、カスタマイズ時の制約も増えます。

選択肢4: 完全自作(採用)

FullCalendar はオーバースペック、react-big-calendar は moment.js 依存。どちらも一長一短で決めきれませんでした。ここで Claude Code に相談しました。

ZeronovaZeronova
カレンダーUIが必要なんだけど、FullCalendar は機能過剰だし、react-big-calendar は moment.js 依存が気になる。必要なのは週表示で空き時間を見せるだけなんだよね。
Claude Code
その要件なら自作も十分選択肢に入ります。「7列の flex コンテナ × 30分単位のスロット」で、コア部分は100行程度で収まるはずです。ライブラリの機能の10%しか使わないなら、依存のコストの方が大きくなります。
ZeronovaZeronova
自作だとアクセシビリティの対応が心配だけど...
Claude Code
確かにライブラリはキーボード操作やスクリーンリーダー対応が最初から入っていることが多いです。ただ、Wakulier の予約画面は「空き時間の表示」が主で、カレンダー上でのイベント操作は不要です。表示用途なら、aria-label を適切に設定するだけで対応できます。

この対話で「ライブラリの機能の10%しか使わないなら自作」という判断基準が明確になりました。結果的に、flexbox で半日で実装でき、FullCalendar のドキュメントを読む時間を考えるとトータルで速かったと感じています。

結局、flexboxで週グリッドを自作。30分単位のスロットを縦に並べるシンプルな実装。

開発日記のこの記述が、最終的な方針でした。

実装 — flexbox で週間スケジュールビュー

実装の核心は「7列のグリッド(週) × 時間スロット(行)」という構造です。

基本レイアウト

function WeekScheduleView({ weekStart, slots }: Props) {
  const days = Array.from({ length: 7 }, (_, i) =>
    addDays(weekStart, i)
  );

  return (
    <div className="flex">
      {/* 時間ラベル列 */}
      <div className="w-16 flex-shrink-0">
        {timeLabels.map((label) => (
          <div key={label} className="h-12 text-xs text-gray-500">
            {label}
          </div>
        ))}
      </div>

      {/* 7日分のカラム */}
      {days.map((day) => (
        <DayColumn key={day.toISOString()} day={day} slots={slots} />
      ))}
    </div>
  );
}

30分スロットの表現

各日のカラムは30分単位のスロットを縦に並べます。空き時間かどうかで色を変えるだけのシンプルな実装です。

function DayColumn({ day, slots }: DayColumnProps) {
  const daySlots = slots.filter(
    (s) => isSameDay(new Date(s.start), day)
  );

  return (
    <div className="flex-1 min-w-0">
      <div className="text-center text-sm font-medium py-2">
        {format(day, "E d", { locale: ja })}
      </div>
      {timeSlots.map((time) => {
        const isAvailable = daySlots.some(
          (s) => getHours(new Date(s.start)) === time.hour
            && getMinutes(new Date(s.start)) === time.minute
        );
        return (
          <div
            key={`${time.hour}-${time.minute}`}
            className={`h-12 border-t ${
              isAvailable
                ? "bg-emerald-500/20 cursor-pointer hover:bg-emerald-500/30"
                : "bg-transparent"
            }`}
          />
        );
      })}
    </div>
  );
}

レスポンシブ対応

モバイルで7日分のカラムをそのまま表示すると幅が足りません。モバイルでは3日表示に切り替え、スワイプで前後の日に移動できるようにしました。

// 画面幅に応じて表示日数を変更
const visibleDays = useMediaQuery("(min-width: 768px)") ? 7 : 3;

自作して良かったこと

バンドルサイズ

ライブラリを追加しなかったため、カレンダー機能分のバンドルサイズ増加はほぼゼロです。コンポーネント自体は100行程度で収まりました。

デザインの一貫性

Wakulier のデザインシステム(Tailwind CSS のカラーパレット、フォントサイズ)をそのまま使えます。ライブラリのデフォルトスタイルを上書きする CSS の格闘が不要でした。

デバッグの容易さ

自分で書いたコードなので、バグが出たときの原因特定が速いです。ライブラリのソースコードを読み解く必要がありません。

自作で苦労したこと

良いことばかりではありませんでした。

週の切り替え

「前の週・次の週」への切り替えを実装する際、日付のバリデーションが意外と面倒でした。4週間先までの制限、過去の週への遷移禁止、表示中の週のハイライトなど、細かいロジックが必要です。

タイムゾーン

Google カレンダー API から返ってくる時間は UTC です。日本時間(JST)での表示に変換する際、日付をまたぐ深夜帯で表示がズレる問題がありました。これは Vercel(UTC)で JST 日時処理にハマった話でも書いた共通の課題です。

アクセシビリティ

キーボード操作でのスロット選択、スクリーンリーダーでの時間帯読み上げなど、アクセシビリティの対応は自作すると自分で実装する必要があります。ライブラリなら最初から対応されていることが多い部分です。

「ライブラリを使わない」判断の基準

今回の経験から、カレンダーに限らず「ライブラリを使うか自作するか」の判断基準を整理しました。

自作が向いているケース

条件理由
要件がシンプルライブラリの機能の10%しか使わない
デザインの自由度が必要ライブラリのスタイル上書きが辛い
バンドルサイズを抑えたいモバイルファーストのプロダクト
依存を増やしたくないメンテナンスコストの削減

ライブラリが向いているケース

条件理由
要件が複雑ドラッグ&ドロップ、リサイズ、繰り返しイベント
チーム開発自作コードのドキュメント整備が負担
アクセシビリティ重要車輪の再発明を避ける
短期間で実装ゼロから作る時間がない

開発日記にはこう書いています:

ライブラリを使わない選択肢も検討すべき

この教訓は、カレンダーに限った話ではありません。「どのライブラリを使うか」を考える前に、「そもそもライブラリが必要か」を問うことが大切です。

ヘッダー設計のおまけ — Google 式2行構造

同じ日の開発日記には、もう一つの学びがありました。

Google式2行ヘッダー: 1行目にサービス名とナビ、2行目にページタイトル。Google Accountの管理画面を参考に。情報の階層が明確になる

ヘッダーの設計も同時に見直し、Google Account の管理画面を参考にした2行構造に変更しました。1行目がグローバルナビ、2行目がページタイトル。情報の階層が明確になり、ユーザーが「今どこにいるか」を把握しやすくなりました。

ヘッダーの構造は「情報の階層」を意識して設計する

UIの設計は、見た目の美しさだけでなく「情報の構造」を伝えることが目的だと再認識した日でした。

関連記事

UIの設計・実装に関連する Journal 記事もどうぞ:

まとめ

カレンダー UI を自作するか、ライブラリを使うか — この判断は「要件の複雑さ」と「プロダクトの優先事項」次第です。

持ち帰りポイント:

  1. 「ライブラリの機能の10%しか使わない」なら自作を検討する価値がある — バンドルサイズと依存管理のコストを天秤にかける
  2. flexbox で週間カレンダーは100行程度で実装できる — 「7列 × 時間スロット」のシンプルな構造
  3. 自作のデメリットはアクセシビリティとエッジケース — これらは自分で全て対応する必要がある
  4. 「どのライブラリを使うか」の前に「ライブラリが必要か」を問う — 要件がシンプルなら自作の方が早いこともある

Wakulier では Claude Code と協力してこのカレンダーコンポーネントを半日で実装できました。FullCalendar のドキュメントを読む時間を考えると、自作の方がトータルで速かったと感じています。もちろんこれは「要件がシンプルだったから」であり、複雑なスケジュール管理が必要なプロダクトでは違う判断になるでしょう。

Zeronova avatar

Zeronovaゼロノバ

Product Manager / AI-Native Builder

Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。

関連プロダクト

Wakulier(ワクリア)

継続案件の依頼管理ツール