Kalender
Ein Datumswähler im Monatsraster für Buchungen, Terminplanung und Bereichsauswahl – mit dem neobrutalistischen Design aus Rahmen und Schatten.
"use client"
import * as React from "react"Der Kalender rendert ein Monatsraster zur Auswahl eines einzelnen Datums, mehrerer Daten oder eines Zeitraums. Er basiert auf React DayPicker – beide Backend-Varianten umhüllen dieselbe Bibliothek – und folgt dem neobrutalistischen Rezept: dicke Rahmen, harte Schatten und kräftige Typografie.
Die richtige Wahl für:
- Datumsfelder in Formularen – kombiniere ihn mit einem Popover zu einem Date Picker für Geburtstage, Deadlines und Liefertermine.
- Buchungsstrecken –
mode="range"für Check-in/Check-out,disabledfür bereits belegte Tage. - Dashboard-Filter – Berichtszeiträume, Verfügbarkeitsansichten und Schnellauswahlen wie „letzte 7 Tage“.
Installation#
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
Installiere die folgenden Abhängigkeiten:
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
Füge die Button-Komponente zu deinem Projekt hinzu.
Die Calendar-Komponente verwendet die Button-Komponente. Stelle sicher, dass sie in deinem Projekt installiert ist.
Kopiere den folgenden Code und füge ihn in dein Projekt ein.
"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 }
Passe die Import-Pfade an die Struktur deines Projekts an.
Verwendung#
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"
/>
)Weitere Informationen findest du in der React DayPicker-Dokumentation.
Über#
Die Calendar-Komponente ist auf React DayPicker aufgebaut.
Date Picker#
Du kannst die <Calendar>-Komponente verwenden, um einen Datumsauswähler zu bauen. Weitere Informationen findest du auf der Date Picker-Seite.
Persischer / Hijri / Jalali Kalender#
Um den persischen Kalender zu verwenden, bearbeite components/ui/calendar.tsx und ersetze react-day-picker durch react-day-picker/persian.
- import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian""use client"
import * as React from "react"Ausgewähltes Datum (mit Zeitzone)#
Die Calendar-Komponente akzeptiert eine Prop timeZone, um sicherzustellen, dass Daten in der lokalen Zeitzone des Nutzers angezeigt und ausgewählt werden.
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}
/>
)
}Hinweis: Wenn dir ein Versatz beim ausgewählten Datum auffällt (zum Beispiel wird beim Auswählen des 20. der 19. hervorgehoben), stelle sicher, dass die Prop timeZone auf die lokale Zeitzone des Nutzers gesetzt ist.
Warum clientseitig? Die Zeitzone wird mit Intl.DateTimeFormat().resolvedOptions().timeZone innerhalb eines useEffect erkannt, um die Kompatibilität mit serverseitigem Rendering sicherzustellen. Die Zeitzone während des Renderings zu erkennen würde zu Hydration-Diskrepanzen führen, da Server und Client in unterschiedlichen Zeitzonen liegen können.
Beispiele#
Basis#
Eine einfache Kalender-Komponente. Wir haben className="rounded border-2 shadow-md" verwendet, um den Kalender zu gestalten.
"use client"
import { Calendar } from "@/components/ui/calendar"Bereichskalender#
Verwende die Prop mode="range", um die Bereichsauswahl zu aktivieren.
"use client"
import * as React from "react"Monats- und Jahresauswahl#
Verwende captionLayout="dropdown", um Dropdowns für Monat und Jahr anzuzeigen.
"use client"
import { Calendar } from "@/components/ui/calendar"Presets#
"use client"
import * as React from "react"Datums- und Zeitauswahl#
"use client"
import * as React from "react"Gebuchte Daten#
"use client"
import * as React from "react"Benutzerdefinierte Zellengröße#
"use client"
import * as React from "react"Du kannst die Größe der Kalenderzellen mit der CSS-Variable --cell-size anpassen. Du kannst sie auch responsiv machen, indem du breakpoint-spezifische Werte verwendest:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>Oder verwende feste Werte:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>Kalenderwochen#
Verwende showWeekNumber, um Kalenderwochen anzuzeigen.
"use client"
import * as React from "react"RTL#
Um die RTL-Unterstützung in Neobrutalism zu aktivieren, siehe die RTL-Konfigurationsanleitung.
Siehe auch die Hijri-Anleitung, um den persischen / Hijri / Jalali Kalender zu aktivieren.
"use client"
import * as React from "react"Wenn du RTL verwendest, importiere die Locale aus react-day-picker/locale und übergib der Calendar-Komponente sowohl die Prop locale als auch die Prop dir:
import { arSA } from "react-day-picker/locale"
;<Calendar
mode="single"
selected={date}
onSelect={setDate}
locale={arSA}
dir="rtl"
/>Barrierefreiheit#
Der Kalender folgt dem Grid-Navigationsmodell aus dem WAI-ARIA-Date-Picker-Dialog-Pattern: React DayPicker rendert den Monat als role="grid" mit jedem Tag als echtem Button und kündigt ausgewählte wie deaktivierte Tage für Screenreader an. Beide Backend-Varianten teilen dieses Verhalten, da sie dieselbe Bibliothek umhüllen.
Tastaturinteraktionen:
| Taste | Aktion |
|---|---|
Tab / Shift + Tab | Bewegt den Fokus zwischen den Navigationsbuttons und dem Tagesraster |
ArrowLeft / ArrowRight | Bewegt den Fokus zum vorherigen / nächsten Tag |
ArrowUp / ArrowDown | Bewegt den Fokus zum selben Wochentag der vorherigen / nächsten Woche |
Home / End | Bewegt den Fokus zum ersten / letzten Tag der Woche |
PageUp / PageDown | Wechselt zum vorherigen / nächsten Monat |
Shift + PageUp / Shift + PageDown | Wechselt zum vorherigen / nächsten Jahr |
Space / Enter | Wählt den fokussierten Tag aus |
API-Referenz#
Weitere Informationen zur Calendar-Komponente findest du in der React DayPicker-Dokumentation.
Changelog#
RTL-Unterstützung#
Wenn du von einer früheren Version der Calendar-Komponente aktualisierst, musst du die folgenden Änderungen anwenden, um Locale-Unterstützung hinzuzufügen:
Importiere den Typ Locale.
Füge Locale zu deinen Imports aus react-day-picker hinzu:
import {
DayPicker,
getDefaultClassNames,
type DayButton,
+ type Locale,
} from "react-day-picker"Füge die Prop locale zur Calendar-Komponente hinzu.
Füge die Prop locale zu den Props der Komponente hinzu:
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
+ locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {Übergib locale an DayPicker.
Übergib die Prop locale an die DayPicker-Komponente:
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(...)}
captionLayout={captionLayout}
+ locale={locale}
formatters={{
formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
+ date.toLocaleString(locale?.code, { month: "short" }),
...formatters,
}}Aktualisiere CalendarDayButton, damit es locale akzeptiert.
Aktualisiere die Signatur der CalendarDayButton-Komponente und übergib locale:
function CalendarDayButton({
className,
day,
modifiers,
+ locale,
...props
- }: React.ComponentProps<typeof DayButton>) {
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {Aktualisiere die Datumsformatierung in CalendarDayButton.
Verwende locale?.code bei der Datumsformatierung:
<Button
variant="ghost"
size="icon"
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toLocaleDateString(locale?.code)}
...
/>Übergib locale an die DayButton-Komponente.
Aktualisiere die Verwendung der DayButton-Komponente, um die Prop locale zu übergeben:
components={{
...
- DayButton: CalendarDayButton,
+ DayButton: ({ ...props }) => (
+ <CalendarDayButton locale={locale} {...props} />
+ ),
...
}}Aktualisiere RTL-fähige CSS-Klassen.
Ersetze richtungsabhängige Klassen durch logische Eigenschaften für bessere RTL-Unterstützung:
// 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)Nachdem du diese Änderungen angewendet hast, kannst du die Prop locale verwenden, um eine locale-spezifische Formatierung bereitzustellen:
import { enUS } from "react-day-picker/locale"
;<Calendar mode="single" selected={date} onSelect={setDate} locale={enUS} />