Calendario
Un selettore di date con griglia mensile per prenotazioni, pianificazione e selezione di intervalli — con lo stile neobrutalista caratterizzato da bordi e ombre.
"use client"
import * as React from "react"Il calendario rende una griglia mensile per scegliere una singola data, più date o un intervallo. Costruito su React DayPicker — entrambe le varianti di backend avvolgono la stessa libreria — e rifinito secondo la ricetta neobrutalista: bordi spessi, ombre nette e tipografia in grassetto.
Quando usarlo:
- Campi data nei moduli — abbinalo a un popover per costruire un Date Picker per compleanni, scadenze e date di consegna.
- Flussi di prenotazione —
mode="range"per il check-in/check-out,disabledper i giorni già prenotati. - Filtri per dashboard — intervalli di date per i report, viste di disponibilità e scorciatoie predefinite come “ultimi 7 giorni”.
Installazione#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/calendar.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/calendar.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/calendar.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/calendar.json
Installa le seguenti dipendenze:
pnpm add react-day-picker date-fnsnpm install react-day-picker date-fnsyarn add react-day-picker date-fnsbun add react-day-picker date-fns
Aggiungi il componente Button al tuo progetto.
Il componente Calendar usa il componente Button. Assicurati di averlo installato nel tuo progetto.
Copia e incolla il seguente codice nel tuo progetto.
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import {
DayPicker,
getDefaultClassNames,
type DayButton,
type Locale,
} from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
function Calendar({
className,
classNames,
showOutsideDays = true,
fixedWeeks = true,
captionLayout = "label",
buttonVariant = "ghost",
locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
fixedWeeks={fixedWeeks}
className={cn(
"group/calendar bg-card p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(8)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
locale={locale}
formatters={{
formatMonthDropdown: (date, dateLib) =>
dateLib
? dateLib.format(date, "LLL")
: date.toLocaleString(locale?.code, { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) rounded border-2 p-0 shadow-sm select-none aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"size-(--cell-size) rounded border-2 p-0 shadow-sm select-none aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"cn-calendar-dropdown-root relative rounded-(--cell-radius)",
defaultClassNames.dropdown_root
),
dropdown: cn(
"absolute inset-0 bg-popover opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"font-head font-medium select-none",
captionLayout === "label"
? "cn-calendar-caption text-sm"
: "cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
defaultClassNames.caption_label
),
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"flex w-(--cell-size) items-center justify-center rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-(--cell-size) select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-[0.8rem] text-muted-foreground select-none",
defaultClassNames.week_number
),
day: cn(
"group/day relative size-(--cell-size) rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
defaultClassNames.day
),
range_start: cn(
"relative isolate z-0 rounded-l-(--cell-radius) bg-primary/30 after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-primary/30",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn(
"relative isolate z-0 rounded-r-(--cell-radius) bg-primary/30 after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-primary/30",
defaultClassNames.range_end
),
today: cn(
"rounded-(--cell-radius) border-2 border-primary text-foreground data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon
className={cn("cn-rtl-flip size-4", className)}
{...props}
/>
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("cn-rtl-flip size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: ({ ...props }) => (
<CalendarDayButton locale={locale} {...props} />
),
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-(--cell-size) items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
locale,
...props
}: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={`${day.date.getFullYear()}-${String(
day.date.getMonth() + 1
).padStart(2, "0")}-${String(day.date.getDate()).padStart(2, "0")}`}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"relative isolate z-10 flex size-(--cell-size) flex-col items-center justify-center gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:outline-2 group-data-[focused=true]/day:outline-offset-2 group-data-[focused=true]/day:outline-primary data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:border-2 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-primary/30 data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:border-2 data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:border-2 data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
Aggiorna i percorsi di import in base alla struttura del tuo progetto.
Utilizzo#
import { Calendar } from "@/components/ui/calendar"const [date, setDate] = React.useState<Date | undefined>(new Date())
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md"
/>
)Per maggiori informazioni consulta la documentazione di React DayPicker.
Informazioni#
Il componente Calendar è costruito sopra React DayPicker.
Date Picker#
Puoi usare il componente <Calendar> per costruire un date picker. Consulta la pagina Date Picker per maggiori informazioni.
Calendario persiano / hijri / jalali#
Per usare il calendario persiano, modifica components/ui/calendar.tsx e sostituisci react-day-picker con react-day-picker/persian.
- import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian""use client"
import * as React from "react"Data selezionata (con fuso orario)#
Il componente Calendar accetta una prop timeZone per garantire che le date siano mostrate e selezionate nel fuso orario locale dell'utente.
export function CalendarWithTimezone() {
const [date, setDate] = React.useState<Date | undefined>(undefined)
const [timeZone, setTimeZone] = React.useState<string | undefined>(undefined)
React.useEffect(() => {
setTimeZone(Intl.DateTimeFormat().resolvedOptions().timeZone)
}, [])
return (
<Calendar
mode="single"
selected={date}
onSelect={setDate}
timeZone={timeZone}
/>
)
}Nota: Se noti uno scarto nella data selezionata (per esempio, selezionando il 20 viene evidenziato il 19), assicurati che la prop timeZone sia impostata sul fuso orario locale dell'utente.
Perché lato client? Il fuso orario viene rilevato usando Intl.DateTimeFormat().resolvedOptions().timeZone all'interno di un useEffect per garantire la compatibilità con il rendering lato server. Rilevare il fuso orario durante il rendering causerebbe disallineamenti di hydration, dato che server e client possono trovarsi in fusi orari diversi.
Esempi#
Base#
Un componente calendario di base. Abbiamo usato className="rounded border-2 shadow-md" per dare stile al calendario.
"use client"
import { Calendar } from "@/components/ui/calendar"Calendario a intervallo#
Usa la prop mode="range" per abilitare la selezione a intervallo.
"use client"
import * as React from "react"Selettore di mese e anno#
Usa captionLayout="dropdown" per mostrare i menu a discesa di mese e anno.
"use client"
import { Calendar } from "@/components/ui/calendar"Preset#
"use client"
import * as React from "react"Selettore di data e ora#
"use client"
import * as React from "react"Date prenotate#
"use client"
import * as React from "react"Dimensione delle celle personalizzata#
"use client"
import * as React from "react"Puoi personalizzare la dimensione delle celle del calendario con la variabile CSS --cell-size. Puoi anche renderla responsive usando valori specifici per ciascun breakpoint:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>Oppure usa valori fissi:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>Numeri di settimana#
Usa showWeekNumber per mostrare i numeri di settimana.
"use client"
import * as React from "react"RTL#
Per abilitare il supporto RTL in Neobrutalism, consulta la guida alla configurazione RTL.
Consulta anche la guida Hijri per abilitare il calendario persiano / hijri / jalali.
"use client"
import * as React from "react"Quando usi RTL, importa la locale da react-day-picker/locale e passa al componente Calendar sia la prop locale che la prop dir:
import { arSA } from "react-day-picker/locale"
;<Calendar
mode="single"
selected={date}
onSelect={setDate}
locale={arSA}
dir="rtl"
/>Accessibilità#
Il calendario segue il modello di navigazione a griglia del pattern WAI-ARIA Date Picker Dialog: React DayPicker rende il mese come role="grid" con ogni giorno come pulsante vero, e annuncia agli screen reader i giorni selezionati e disabilitati. Entrambe le varianti di backend condividono questo comportamento perché avvolgono la stessa libreria.
Interazioni da tastiera:
| Tasto | Azione |
|---|---|
Tab / Shift + Tab | Sposta il focus tra i pulsanti di navigazione e la griglia dei giorni |
ArrowLeft / ArrowRight | Sposta il focus sul giorno precedente / successivo |
ArrowUp / ArrowDown | Sposta il focus sullo stesso giorno della settimana precedente / successiva |
Home / End | Sposta il focus sul primo / ultimo giorno della settimana |
PageUp / PageDown | Passa al mese precedente / successivo |
Shift + PageUp / Shift + PageDown | Passa all’anno precedente / successivo |
Space / Enter | Seleziona il giorno con il focus |
Riferimento API#
Per maggiori informazioni sul componente Calendar, consulta la documentazione di React DayPicker.
Registro delle modifiche#
Supporto RTL#
Se stai aggiornando da una versione precedente del componente Calendar, dovrai applicare le seguenti modifiche per aggiungere il supporto alle locale:
Importa il tipo Locale.
Aggiungi Locale ai tuoi import da react-day-picker:
import {
DayPicker,
getDefaultClassNames,
type DayButton,
+ type Locale,
} from "react-day-picker"Aggiungi la prop locale al componente Calendar.
Aggiungi la prop locale alle prop del componente:
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
+ locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {Passa locale a DayPicker.
Passa la prop locale al componente DayPicker:
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(...)}
captionLayout={captionLayout}
+ locale={locale}
formatters={{
formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
+ date.toLocaleString(locale?.code, { month: "short" }),
...formatters,
}}Aggiorna CalendarDayButton per accettare locale.
Aggiorna la firma del componente CalendarDayButton e passa locale:
function CalendarDayButton({
className,
day,
modifiers,
+ locale,
...props
- }: React.ComponentProps<typeof DayButton>) {
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {Aggiorna la formattazione delle date in CalendarDayButton.
Usa locale?.code nella formattazione delle date:
<Button
variant="ghost"
size="icon"
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toLocaleDateString(locale?.code)}
...
/>Passa locale al componente DayButton.
Aggiorna l'uso del componente DayButton per passare la prop locale:
components={{
...
- DayButton: CalendarDayButton,
+ DayButton: ({ ...props }) => (
+ <CalendarDayButton locale={locale} {...props} />
+ ),
...
}}Aggiorna le classi CSS compatibili con RTL.
Sostituisci le classi direzionali con proprietà logiche per un supporto RTL migliore:
// In the day classNames:
- [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)
+ [&:last-child[data-selected=true]_button]:rounded-e-(--cell-radius)
- [&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)
+ [&:nth-child(2)[data-selected=true]_button]:rounded-s-(--cell-radius)
- [&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)
+ [&:first-child[data-selected=true]_button]:rounded-s-(--cell-radius)
// In range_start classNames:
- rounded-l-(--cell-radius) ... after:right-0
+ rounded-s-(--cell-radius) ... after:end-0
// In range_end classNames:
- rounded-r-(--cell-radius) ... after:left-0
+ rounded-e-(--cell-radius) ... after:start-0
// In CalendarDayButton className:
- data-[range-end=true]:rounded-r-(--cell-radius)
+ data-[range-end=true]:rounded-e-(--cell-radius)
- data-[range-start=true]:rounded-l-(--cell-radius)
+ data-[range-start=true]:rounded-s-(--cell-radius)Dopo aver applicato queste modifiche, puoi usare la prop locale per fornire una formattazione specifica per ciascuna locale:
import { enUS } from "react-day-picker/locale"
;<Calendar mode="single" selected={date} onSelect={setDate} locale={enUS} />