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.

RepresentationWhere it livesCost
YYYY-MM-DD stringField state, form, databaseNo time zone, no time of day
DateOnly inside the calendar and the formattingDepends on the browser time zone
ISO with a time and ZThe timestamp of an event, not of a dayNeeds 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" with numberOfMonths={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.