Calendar
Siatka miesiąca do wyboru jednej daty, kilku dat lub zakresu — podstawa każdego selektora daty.
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
// A fixed "today" keeps statically rendered docs deterministic.
const today = new Date(2026, 8, 23)
export default function CalendarDemo() {
const [date, setDate] = React.useState<Date | undefined>(
new Date(2026, 8, 26)
)
return (
<Calendar
mode="single"
today={today}
defaultMonth={today}
selected={date}
onSelect={setDate}
className="rounded-xl border shadow-raised"
/>
)
}Anatomia
- 1NagłówekWyświetlany miesiąc. W nagłówku z listami rozwijanymi zamienia się w listy wyboru miesiąca i roku.
- 2DzisiajOznaczony subtelnym wypełnieniem, więc łatwo go znaleźć, a nie wygląda na zaznaczony.
- 3NawigacjaPoprzedni i następny miesiąc. Na krańcach dozwolonego zakresu przyciski się wyłączają.
- 4Zaznaczony dzieńWypełniony kolorem marki. W zakresach zabarwione są też dni pomiędzy.
- 5Dzień tygodniaSkrócone nagłówki kolumn, od pierwszego dnia tygodnia według ustawień regionalnych.
Instalacja
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/calendar.jsonUżycie
import { Calendar } from "@/components/ui/calendar"const [date, setDate] = React.useState<Date | undefined>(new Date())
<Calendar mode="single" selected={date} onSelect={setDate} />Calendar opakowuje React DayPicker w style prfct. Zaznaczenie korzysta ze skali marki: zaznaczone dni wypełnia brand-9, dni wewnątrz zakresu — brand-3, a dzisiejszy dzień jest pogrubiony na spokojnym tle.
today, żeby strony renderowane statycznie zgadzały się z tym, co wyrenderuje przeglądarka. W swojej aplikacji go pomiń.Przykłady
Zakres
mode="range" wybiera początek i koniec; numberOfMonths={2} pokazuje oba krańce dłuższych zakresów naraz. Przeszłe dni wyłącz przez disabled={{ before: today }}, zamiast sprawdzać wybór po fakcie.
"use client"
import * as React from "react"
import type { DateRange } from "react-day-picker"
import { Calendar } from "@/components/ui/calendar"
const today = new Date(2026, 8, 23)
export default function CalendarRange() {
const [range, setRange] = React.useState<DateRange | undefined>({
from: new Date(2026, 8, 28),
to: new Date(2026, 9, 9),
})
return (
<Calendar
mode="range"
today={today}
defaultMonth={today}
numberOfMonths={2}
selected={range}
onSelect={setRange}
disabled={{ before: today }}
className="rounded-xl border shadow-raised"
/>
)
}Wiele dat
mode="multiple" przełącza pojedyncze dni. Ogranicz wybór propem max i pokaż licznik, żeby użytkownik wiedział, kiedy dojdzie do limitu.
Wybrano: 3 z 5 dni w biurze
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
const today = new Date(2026, 8, 23)
export default function CalendarMultiple() {
const [days, setDays] = React.useState<Date[] | undefined>([
new Date(2026, 8, 24),
new Date(2026, 8, 29),
new Date(2026, 9, 1),
])
return (
<div className="flex flex-col items-center gap-3">
<Calendar
mode="multiple"
max={5}
today={today}
defaultMonth={today}
selected={days}
onSelect={setDays}
className="rounded-xl border shadow-raised"
/>
<p className="text-sm text-muted-foreground" aria-live="polite">
{`Wybrano: ${days?.length ?? 0} z 5 dni w biurze`}
</p>
</div>
)
}Listy rozwijane miesiąca i roku
Przy datach odległych od dzisiejszej — takich jak data urodzenia czy rok ważności — captionLayout="dropdown" zastępuje nagłówek listami wyboru miesiąca i roku. Ogranicz ich zakres propami startMonth i endMonth.
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
export default function CalendarDropdown() {
const [date, setDate] = React.useState<Date | undefined>(
new Date(1990, 4, 12)
)
return (
<Calendar
mode="single"
captionLayout="dropdown"
startMonth={new Date(1920, 0)}
endMonth={new Date(2026, 11)}
defaultMonth={date}
selected={date}
onSelect={setDate}
className="rounded-xl border shadow-raised"
/>
)
}Niedostępne daty
disabled przyjmuje daty, zakresy, dni tygodnia i funkcje. Połącz go z modifiers, żeby pokazać, dlaczego dzień jest niedostępny — tutaj zarezerwowane dni są przekreślone, a weekendy tylko przygaszone.
"use client"
import * as React from "react"
import { Calendar } from "@/components/ui/calendar"
const today = new Date(2026, 8, 23)
const booked = [
new Date(2026, 8, 24),
new Date(2026, 8, 25),
new Date(2026, 9, 2),
]
export default function CalendarDisabledDates() {
const [date, setDate] = React.useState<Date | undefined>()
return (
<Calendar
mode="single"
today={today}
defaultMonth={today}
selected={date}
onSelect={setDate}
// Past days, weekends and fully booked days can't be chosen.
disabled={[{ before: today }, { dayOfWeek: [0, 6] }, ...booked]}
modifiers={{ booked }}
modifiersClassNames={{ booked: "[&>button]:line-through" }}
className="rounded-xl border shadow-raised"
/>
)
}Selektor daty
Codzienne pole daty: Button, który pokazuje wybraną datę i otwiera kalendarz w komponencie Popover. Zamykaj popover po wyborze i formatuj datę dla ludzi, nie dla maszyn.
Po tym dniu zadanie stanie się zaległe.
"use client"
import * as React from "react"
import { CalendarIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
const today = new Date(2026, 8, 23)
const formatter = new Intl.DateTimeFormat("pl-PL", { dateStyle: "long" })
export default function CalendarDatePicker() {
const [open, setOpen] = React.useState(false)
const [date, setDate] = React.useState<Date | undefined>()
return (
<Field className="w-64">
<FieldLabel htmlFor="due-date">Termin</FieldLabel>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger
render={
<Button
id="due-date"
variant="outline"
className="w-full justify-start font-normal data-[empty=true]:text-muted-foreground"
data-empty={!date}
/>
}
>
<CalendarIcon data-icon="inline-start" />
{date ? formatter.format(date) : "Wybierz datę"}
</PopoverTrigger>
<PopoverContent align="start" className="w-auto p-0">
<Calendar
mode="single"
today={today}
defaultMonth={date ?? today}
selected={date}
onSelect={(next) => {
setDate(next)
setOpen(false)
}}
disabled={{ before: today }}
/>
</PopoverContent>
</Popover>
<FieldDescription>
Po tym dniu zadanie stanie się zaległe.
</FieldDescription>
</Field>
)
}Lokalizacja
Przekaż locale z react-day-picker/locale, żeby przetłumaczyć nazwy miesięcy i dni oraz zaczynać tydzień od właściwego dnia. Tutaj po polsku — tydzień zaczyna się w poniedziałek — z numerami tygodni ISO.
"use client"
import * as React from "react"
import { pl } from "react-day-picker/locale"
import { Calendar } from "@/components/ui/calendar"
const today = new Date(2026, 8, 23)
export default function CalendarLocalized() {
const [date, setDate] = React.useState<Date | undefined>(
new Date(2026, 8, 30)
)
return (
<Calendar
mode="single"
locale={pl}
showWeekNumber
today={today}
defaultMonth={today}
selected={date}
onSelect={setDate}
className="rounded-xl border shadow-raised"
/>
)
}Wytyczne
Kiedy używać
- Do wyboru dat bliskich dzisiejszej, gdy pomaga kontekst tygodnia i miesiąca: rezerwacje, terminy, dostępność.
- Do wyboru zakresów, gdy widok obu krańców naraz zapobiega pomyłkom.
Kiedy nie używać
- Do dat, które użytkownik zna na pamięć i które są odległe od dzisiejszej, np. daty urodzenia — trzy pola albo pole tekstowe z czytelnym formatem są szybsze niż cofanie się o dziesięciolecia.
- Do przybliżonych terminów, takich jak w przyszłym tygodniu — zaproponuj gotowe opcje w komponencie Select albo przyciskami.
Pokazuj ograniczenia
Wyłącz dni, których nie można wybrać, zamiast odrzucać je po wyborze, i wyjaśnij powód w tekście obok (Weekendy są niedostępne). Nigdy nie pozwól zbudować wyboru, który zostanie odrzucony dopiero przy wysyłaniu formularza.
Formaty
W wyzwalaczu pokazuj datę słownie (26 września 2026) za pomocą Intl.DateTimeFormat z ustawieniami regionalnymi użytkownika, a zapisuj ją w ISO 8601. Niejednoznaczne formaty liczbowe, takie jak 09/10, w różnych krajach czyta się różnie.
Dostępność
Kalendarz renderuje grid przycisków z wędrującym fokusem (roving focus). Czytnik ekranu odczytuje każdy dzień z pełną datą oraz stanem zaznaczenia i wyłączenia; nagłówek jest regionem na żywo, więc podczas nawigacji ogłaszany jest bieżący miesiąc.
| Klawisz | Działanie |
|---|---|
←→ | Przenosi fokus na poprzedni lub następny dzień. |
↑↓ | Przenosi fokus na ten sam dzień w poprzednim lub następnym tygodniu. |
PageUpPageDown | Przechodzi do tego samego dnia w poprzednim lub następnym miesiącu. |
ShiftPageUpShiftPageDown | Przechodzi do tego samego dnia w poprzednim lub następnym roku. |
HomeEnd | Przechodzi do pierwszego lub ostatniego dnia tygodnia. |
EnterSpacja | Zaznacza dzień, na którym jest fokus. |
- Fokus. Dzień z fokusem ma standardowy pierścień fokusu o grubości 2 px; zaznaczony dzień ma dodatkowo
aria-selected— nigdy nie jest oznaczony samym kolorem. - W popoverze. Tekst przycisku wyzwalacza powinien podawać bieżącą wartość (Wybierz datę albo sformatowaną datę), a po zamknięciu popovera fokus wraca na ten przycisk.
- Kontrast. Zaznaczone dni używają
brand-9z kolorem kontrastowym marki (≥ 4,5:1); dni spoza miesiąca i dni wyłączone są przygaszone, ale pozostają czytelne.
Dokumentacja API
Calendar
Przyjmuje wszystkie propsy DayPicker. Najczęściej używane:
CalendarDayButton
Przycisk renderowany dla każdego dnia. Podmień go przez components={{ DayButton }}, żeby dodać treść, np. ceny albo kropki dostępności.