🦩

react-day-pickerに日本の祝日データを読み込む

に公開

react-day-pickerに祝日の場合のスタイルを設定した際の記録メモです
https://daypicker.dev/
react-day-pickerにある、data-*属性を利用します
https://developer.mozilla.org/ja/docs/Web/HTML/How_to/Use_data_attributes

環境

nextjsのcreate-next-app@latestを実行してプロジェクトを作成
https://nextjs.org/docs/app/getting-started/installation
プロジェクト設定はこちら

% npx create-next-app@latest
Need to install the following packages:
create-next-app@15.4.5
Ok to proceed? (y) y
✔ What is your project named? … example-app
✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … Yes
✔ Would you like your code inside a `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to use Turbopack for `next dev`? … Yes
✔ Would you like to customize the import alias (`@/*` by default)? … No

react-day-pickerをインストール

npm install react-day-picker

祝日データをダウンロードしてpublicフォルダに格納した状態
https://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv

デバック実行

npm run dev

完成後画面キャプチャ

祝日データを反映したカレンダー

実装

共通利用できるようコンポーネント化
selected, onChangeは親コンポーネントから呼び出すようにします

src/app/ui/dayPicker.tsx
import { useState, useEffect, Dispatch, SetStateAction } from "react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { ja } from 'react-day-picker/locale';
import 'react-day-picker/style.css';
import { format, addYears } from "date-fns";

export default function DayPicker({selected, onChange} :{ selected?: Date, onChange: Dispatch<SetStateAction<Date | undefined>>, }) {
  const [holidays, setHolidays] = useState<{ [date: string]: string }>({});

  const defaultClassNames = getDefaultClassNames();

  useEffect(() => {
    // 祝日データを取得する
  }, []);

  return (
      <DayPicker
        month={selected}
        navLayout="around"
        onMonthChange={setSelected}
        animate
        mode="single"
        locale={ja}
        selected={
          selected
        }
        onSelect={setSelected}
        // 祝日データの期間が1955年からダウンロードした年の翌年までなので、翌年の祝日はカレンダー選択させない
        disabled={[
          {
            after: addYears(new Date(new Date().getFullYear(), 11, 31), 1),
          }
        ]}
        components={{
          DayButton: (props) => {
            return <div className="relative">
              <DayButton {...props} className={`${defaultClassNames.day_button}`} />
              {holidays && <span className="absolute text-[10px] top-[32px] left-0 w-(--rdp-day_button-width)">
                {holidays[format(props.day.date, 'yyyy-MM-dd')]}
              </span>}
            </div>
          }
        }}
        classNames={{
          today: `text-[#001319]`,
          weekday: `${defaultClassNames.weekday} first:text-[#F73737] last:text-[#0064CA]`,
          day: `${defaultClassNames.day} first:text-[#F73737] last:text-[#0064CA] <祝日の場合、文字色を変更するスタイル文字列>`,
        }}
      />
  );
}

祝日データを取得して、使いやすいデータに整形する

syukujitsu.csvのデータをobject型に整形します、
コンポーネント内に書く場合、fetchのURLは'/'スラッシュ始まりで、publicフォルダにアクセスできます

fetch('/syukujitsu.csv').then(res => {
  // 配布されているファイル文字コードがshiftjisなので、いったんarraybufferに変換したのち、デコードする
  res.arrayBuffer().then((data) => {
    const decoded = new TextDecoder('shift_jis').decode(data);
    // ヘッダーを除く
    setHolidays(decoded.split(/\r\n/).filter(x => x).slice(1).map((holiday) => {
      const [date, name] = holiday.split(',');
      // data-*属性値にあわせてフォーマットする
      // ファイルには「yyyy/MM/dd,"〇〇の日"」とあるので、変換する
      return {
         [format(new Date(date.split('/').join()), 'yyyy-MM-dd')]: name
      }
    }).reduce((x, y) => { return { ...x, ...y }; }, {}));
  });
});

結果

{
        "1955-01-01": "元日",
        "1955-01-15": "成人の日",
...省略
        "2026-11-03": "文化の日",
        "2026-11-23": "勤労感謝の日"
},

祝日データにスタイルを指定する

tailwindcssでは動的なクラス指定はできません
https://tailwindcss.com/docs/detecting-classes-in-source-files#dynamic-class-names

そのため、スタイルクラス名を明示的に指定します

その1

data-*属性を利用するためにスタイル文字列を生成して、上記の「<祝日の場合、文字色を変更するスタイル文字列>」の箇所にペーストします

Object.keys(holidays).map((date) => {
  return `data-[day="${date}"]:text-[#F73737]`
}).join(' ');

その2

cssファイルにスタイルを書き込むAPIをつくり、スタイルクラスを指定します
https://nextjs.org/docs/app/api-reference/file-conventions/route
書き込むcssファイル名はとくに指定ありません
インポートエラーになるため、事前に書き込むcssファイルを作成しておきます

src/app/api/holiday/route.ts
import { readFileSync, writeFileSync } from "fs";
import { format } from "date-fns";

export async function GET() {
  // NOTE: 補足参照
  const syukujitsu = await readFileSync('public/syukujitsu.csv');
  const decoded = new TextDecoder('shift_jis').decode(syukujitsu);
  const data = decoded.split(/\r\n/).filter(x => x).slice(1).map((holiday) => {
    const [date, name] = holiday.split(',');
    return {
      [format(new Date(date.split('/').join()), 'yyyy-MM-dd')]: name
    }
  }).reduce((x, y) => { return { ...x, ...y }; }, {})
  const style = Object.keys(data).map((date) => {
    return `td.rdp-day[data-day="${date}"]`;
  }).join(',') + '{ color: #F73737; }'

  try {
    writeFileSync('src/app/ui/day_picker.css', style);
  } catch (error) {
    return Response.json({ data: data, message: error });
  }
  return Response.json({ data: data, message: 'success' });
}

tsxファイルにcssファイルをインポートします

src/app/ui/dayPicker.tsx
import 'react-day-picker/style.css';
import './day_picker.css'

fetchで祝日データ取得とあわせてリクエストします

src/app/ui/dayPicker.tsx
useEffect(() => {
  fetch('/api/holiday').then(async (res) => {
    const response = await res.json();
    setHolidays(response.data);
  });
}, []);

画面に描画

page.tsxにコンポーネントを追加します

src/app/page.tsx
'use client';

import { useState } from "react";
import DayPicker from "./ui/dayPicker";

export default function Home() {
  const [selected, setSelected] = useState<Date>();
  return (
    <DayPicker selected={selected} onChange={setSelected} />
  );
}

補足

route.tsでファイル読み込みする際、プロジェクトのトップディレクトリから指定するようにします

Error: ENOENT: no such file or directory, open '/syukujitsu.csv'
    at GET (src/app/api/holiday/route.ts:22:40)
  20 |
  21 | export async function GET() {
> 22 |   const syukujitsu = await readFileSync('/syukujitsu.csv');
     |                                        ^
  23 |   const decoded = new TextDecoder('shift_jis').decode(syukujitsu);
  24 |   const data = decoded.split(/\r\n/).filter(x => x).slice(1).map((holiday) => {
  25 |     const [date, name] = holiday.split(','); {
  errno: -2,
  code: 'ENOENT',
  syscall: 'open',
  path: '/syukujitsu.csv'
}

Discussion