🦩
react-day-pickerに日本の祝日データを読み込む
react-day-pickerに祝日の場合のスタイルを設定した際の記録メモです react-day-pickerにある、data-*属性を利用します
環境
nextjsのcreate-next-app@latestを実行してプロジェクトを作成 プロジェクト設定はこちら
% 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フォルダに格納した状態
デバック実行
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では動的なクラス指定はできません
そのため、スタイルクラス名を明示的に指定します
その1
data-*属性を利用するためにスタイル文字列を生成して、上記の「<祝日の場合、文字色を変更するスタイル文字列>」の箇所にペーストします
Object.keys(holidays).map((date) => {
return `data-[day="${date}"]:text-[#F73737]`
}).join(' ');
その2
cssファイルにスタイルを書き込むAPIをつくり、スタイルクラスを指定します
書き込む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