React
shadcn calendar and date picker: dates, state and time zones
The shadcn Calendar is react-day-picker inside a Popover: keep the date as a YYYY-MM-DD string and turn it into a Date only in the interface.
Key points
- The shadcn `Calendar` is react-day-picker wrapped in a file you own, so upstream updates never arrive.
- A date picker is an outline `Button` that opens the `Calendar` inside a `Popover`.
- Keep the value as a `YYYY-MM-DD` string: a `Date` carries the time zone of the browser that built it.
Short answer: the shadcn Calendar is react-day-picker wrapped in a component you own, and the date picker is that calendar inside a Popover. The value you store is a YYYY-MM-DD string, not a Date.
What the command adds
The CLI copies calendar.tsx and popover.tsx into components/ui, adds react-day-picker and date-fns to the dependencies and touches nothing else. From then on the file is yours: shadcn updates never arrive.
npx shadcn@latest add calendar popover button
The field, not just the grid
A calendar on its own is not a field: the readable value, the placeholder and the link to the form are missing. The pattern is an outline Button that shows the formatted date and opens the calendar.
"use client";
import { useState } from "react";
import { format } from "date-fns";
import { Button } from "@/components/ui/button";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
export function DateField({ name, label }: { name: string; label: string }) {
const [value, setValue] = useState("");
// Local midnight, not UTC midnight: the string is a day, not an instant.
const selected = value ? new Date(value + "T00:00:00") : undefined;
return (
{selected ? format(selected, "PPP") : "Scegli una data"}
setValue(day ? format(day, "yyyy-MM-dd") : "")}
/>
);
}
The string, not a Date object
The value that crosses the form is a string: 2026-11-01 is the same day in Milan, in New York and on the server. A Date carries an instant instead, and the instant depends on the time zone of whoever creates it.
| Representation | Where it lives | Cost |
|---|---|---|
YYYY-MM-DD string | Field state, form, database | No time zone, no time of day |
Date | Only inside the calendar and the formatting | Depends on the browser time zone |
ISO with a time and Z | The timestamp of an event, not of a day | Needs the user's time zone |
The three variants that cover almost everything
- Single date:
mode="single", the case of a filter, a deadline and a form field. - Range:
mode="range"withnumberOfMonths={2}for billing periods. - Blocked days:
disabled={{ before: new Date() }}closes the past,disabled={{ dayOfWeek: [0, 6] }}closes weekends.
Source: the shadcn/ui Calendar page (MIT licence) and the react-day-picker repository (MIT licence); the PPP and yyyy-MM-dd formats come from date-fns, also MIT.
A named limitation
The calendar draws days in the browser time zone, and the server has none. While the value stays a string the two agree; as soon as you write new Date("2026-11-01") on a server west of Greenwich, JavaScript reads UTC midnight and the day steps back by one. A second limit: blocked days are interface, not a rule. A user can still submit the Sunday you closed, so validation has to run again on the server.
Where to continue
In the catalogue you find the shadcn library, a calendar block with preview and the form category blocks. To understand what you are installing, read what shadcn/ui is and how to theme shadcn; the comparison of ready-made themes is free versus premium shadcn themes. When the date comes from the server and React complains, the cause is in hydration mismatches on dates; for long lists the sibling of this article is a searchable select.
Frequently asked questions
Do I need react-day-picker to use Calendar?
Yes. `calendar.tsx` is a wrapper around react-day-picker (10.0.2 on 1 November 2026) with Tailwind classes: without that dependency the file does not compile.
The saved date is one day off. Why?
Almost always because of `new Date("2026-11-01")`, which JavaScript reads as UTC midnight. Pass an explicit local time: `new Date("2026-11-01T00:00:00")`.
How do I build a date range?
With `mode="range"`: `onSelect` returns an object with `from` and `to`. Keep one month on mobile, two side by side on desktop with `numberOfMonths={2}`.
The popover closes when I click a day. Is that a bug?
No, it is the intended behaviour: the click selects and the popover loses focus. To keep it open, control `open` and ignore `onOpenChange` when the selection is valid.
Can I use the calendar inside an uncontrolled form?
Yes, with a hidden input that carries the `name`: the value you submit is the formatted string, not the `Date` object.