📆

React 19×shadcn uiで日時入力可能なカレンダーコンポーネントを作る

に公開

1.はじめに

今回はReact 19 × shadcn uiで日時入力可能なカレンダーコンポーネントを作成してみました。
イメージは以下の通り、クリックすると日時入力が選択できるようなコンポーネントです。

■ 経緯:react-day-picker(8.10.1)がReact 19に対応してない

React 19 × shadcn uiでカレンダーのコンポーネントを使おうと思い
公式ドキュメント通りに「react-day-picker」のインストールを試みたところ以下エラーが出ました。
どうやら、react-day-picker8.10.1React 19に対応していないそうです。(2025/6時点)

https://ui.shadcn.com/docs/components/calendar

$ npm install react-day-picker@8.10.1 date-fns
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency tree
npm error While resolving: react-tutorial@0.1.0
npm error Found: react@19.1.0
npm error node_modules/react
npm error   react@"^19.0.0" from the root project
npm error Could not resolve dependency:
npm error peer react@"^16.8.0 || ^17.0.0 || ^18.0.0" from react-day-picker@8.10.1
npm error node_modules/react-day-picker
npm error   react-day-picker@"8.10.1" from the root project
npm error Fix the upstream dependency conflict, or retry
npm error this command with --force or --legacy-peer-deps
npm error to accept an incorrect (and potentially broken) dependency resolution.

■ 手順

前述の経緯のため、以下手順で日時入力可能なカレンダーコンポーネントを作ることになりました。

  • React 19に対応している「react-day-picker」を入れる
  • 上記「react-day-picker」をshadcnのCalendarコンポーネントに対応させる(=日付入力まで)
  • 日時が入力できるようにカスタマイズ

2.React 19に対応した「react-day-picker」を導入する

どうやら同じ現象に悩まされている人がいるみたいで、
React 19に対応している「react-day-picker」のバージョンは「9.6.7」ということが分かりました。
ということでインストール。(npmの場合)

npm install react-day-picker@9.6.7 date-fns

3.「react-day-picker@9.6.7」をshadcn uiに対応させる

公式ドキュメント通りに、「components/ui/calender.tsx」を作成していきます。
そのままコピペすると、以下のように見た目が崩れます。(2025/06時点)

なので、以下のように修正します。

components/ui/calender.tsx

全体のコード
import * as React from "react";
import { DayPicker, getDefaultClassNames } from "react-day-picker";

import { cn } from "@/lib/utils";
import { buttonVariants } from "./button";

function Calendar({
  className,
  classNames,
  showOutsideDays = true,
  ...props
}: React.ComponentProps<typeof DayPicker>) {
  const defaultClassNames = getDefaultClassNames();
  return (
    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn("p-3", className)}
      classNames={{
        months: `relative flex ${defaultClassNames.month}`,
        month_caption: `relative mx-10 flex h-7 items-center justify-center ${defaultClassNames.month_caption}`,
        weekdays: cn("flex flex-row", classNames?.weekdays),
        weekday: cn(
          "w-8 text-sm font-normal text-muted-foreground",
          classNames?.weekday
        ),
        month: cn("w-full", classNames?.month),

        caption_label: cn(
          "truncate text-sm font-medium",
          classNames?.caption_label
        ),
        button_next: cn(
          buttonVariants({ variant: "outline" }),
          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute right-1 [&_svg]:fill-foreground",
          classNames?.button_next
        ),
        button_previous: cn(
          buttonVariants({ variant: "outline" }),
          "h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 absolute left-1 [&_svg]:fill-foreground",
          classNames?.button_previous
        ),
        nav: cn("flex items-start", classNames?.nav),
        month_grid: cn("mx-auto mt-4", classNames?.month_grid),
        week: cn("mt-2 flex w-max items-start", classNames?.week),
        day: cn(
          "flex size-8 flex-1 items-center justify-center p-0 text-sm",
          classNames?.day
        ),
        day_button: cn(
          "size-8 rounded-md p-0 font-normal transition-none aria-selected:opacity-100",
          classNames?.day_button
        ),
        range_start: cn(
          "bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground day-range-start rounded-s-md",
          classNames?.range_start
        ),
        range_middle: cn(
          "bg-accent !text-foreground [&>button]:bg-transparent [&>button]:!text-foreground [&>button]:hover:bg-transparent [&>button]:hover:!text-foreground",
          classNames?.range_middle
        ),
        range_end: cn(
          "bg-accent [&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground day-range-end rounded-e-md",
          classNames?.range_end
        ),
        selected: cn(
          "[&>button]:bg-primary [&>button]:text-primary-foreground [&>button]:hover:bg-primary [&>button]:hover:text-primary-foreground",
          classNames?.selected
        ),
        today: cn(
          "[&>button]:bg-accent [&>button]:text-accent-foreground",
          classNames?.today
        ),
        outside: cn(
          "day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
          classNames?.outside
        ),
        disabled: cn("text-muted-foreground opacity-50", classNames?.disabled),
        hidden: cn("invisible flex-1", classNames?.hidden),
        ...classNames,
      }}
      {...props}
    />
  );
}
Calendar.displayName = "Calendar";

export { Calendar };

上記のように修正すると以下の形で利用できるかと思います。

<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
  className="rounded-md border shadow-sm"
/>

4.日時入力可能なコンポーネントを作成する

作成した「Calender」コンポーネントと他のshadcnコンポーネントを組み合わせて、日時入力可能なコンポーネントを作成します。

components/ui/date-time-picker.tsx

全体のコード
"use client";

import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calender";

import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import { format } from "date-fns";
import { CalendarIcon } from "lucide-react";
import * as React from "react";

export function DatetimePicker({
  value,
  onChange,
  className,
}: {
  value: Date | undefined;
  onChange: (date: Date) => void;
  className?: string;
}) {
  const [open, setOpen] = React.useState(false);

  return (
    <div className={cn("flex flex-col space-y-1", className)}>
      <Popover open={open} onOpenChange={setOpen}>
        <PopoverTrigger asChild>
          <Button
            variant="outline"
            className={cn(
              "justify-start text-left font-normal",
              !value && "text-muted-foreground"
            )}
          >
            <CalendarIcon className="mr-2 h-4 w-4" />
            {value ? format(value, "yyyy/MM/dd HH:mm") : "yyyy/mm/dd --:--"}
          </Button>
        </PopoverTrigger>
        <PopoverContent className="w-auto p-0">
          <Calendar
            mode="single"
            selected={value}
            onSelect={(date) => {
              if (date) {
                const current = value ?? new Date();
                const newDate = new Date(
                  date.getFullYear(),
                  date.getMonth(),
                  date.getDate(),
                  current.getHours(),
                  current.getMinutes()
                );
                onChange(newDate);
                setOpen(false);
              }
            }}
            showOutsideDays
          />
          <div className="p-4 flex items-center space-x-2 border-t">
            <input
              type="time"
              className="border rounded px-2 py-1 text-sm"
              value={value ? format(value, "HH:mm") : ""}
              onChange={(e) => {
                if (value) {
                  const [h, m] = e.target.value.split(":").map(Number);
                  const updated = new Date(value);
                  updated.setHours(h);
                  updated.setMinutes(m);
                  onChange(updated);
                }
              }}
            />
          </div>
        </PopoverContent>
      </Popover>
    </div>
  );
}

上記作成すると以下の形で利用できます。

<DatetimePicker
  value={start}
  onChange={setStart}
/>

見た目は最初に紹介したものになります。

Discussion