Kalender
Een datumkiezer met maandoverzicht voor boekingen, planning en het selecteren van periodes — met die neobrutalistische stijl van randen en schaduwen.
"use client"
import * as React from "react"De kalender rendert een maandraster om één datum, meerdere datums of een bereik te kiezen. Gebouwd op React DayPicker — de varianten voor beide backends wikkelen dezelfde library — en vormgegeven volgens het neobrutalistische recept: dikke randen, harde schaduwen en vette typografie.
Gebruik deze component bij:
- Datumvelden in formulieren — combineer de kalender met een popover tot een Date Picker voor geboortedata, deadlines en bezorgdatums.
- Boekingsflows —
mode="range"voor check-in/check-out,disabledvoor al geboekte dagen. - Dashboardfilters — rapportageperiodes, beschikbaarheidsweergaven en voorinstellingen zoals "afgelopen 7 dagen".
Installatie#
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
Installeer de volgende afhankelijkheden:
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
Voeg de component Button toe aan je project.
De component Calendar gebruikt de component Button. Zorg ervoor dat je die in je project hebt geïnstalleerd.
Kopieer en plak de volgende code in je project.
"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 }
Werk de import-paden bij zodat ze overeenkomen met je projectstructuur.
Gebruik#
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"
/>
)Zie de React DayPicker-documentatie voor meer informatie.
Over#
De component Calendar is gebouwd bovenop React DayPicker.
Date Picker#
Je kunt de component <Calendar> gebruiken om een datumkiezer te bouwen. Zie de pagina Date Picker voor meer informatie.
Perzische / hijri / jalali-kalender#
Om de Perzische kalender te gebruiken, bewerk je components/ui/calendar.tsx en vervang je react-day-picker door react-day-picker/persian.
- import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian""use client"
import * as React from "react"Geselecteerde datum (met tijdzone)#
De Calendar-component accepteert een prop timeZone om ervoor te zorgen dat datums worden weergegeven en geselecteerd in de lokale tijdzone van de gebruiker.
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}
/>
)
}Let op: Als je een verschuiving in de geselecteerde datum opmerkt (bijvoorbeeld: het selecteren van de 20e markeert de 19e), zorg er dan voor dat de prop timeZone is ingesteld op de lokale tijdzone van de gebruiker.
Waarom aan de clientzijde? De tijdzone wordt gedetecteerd met Intl.DateTimeFormat().resolvedOptions().timeZone binnen een useEffect om compatibiliteit met server-side rendering te garanderen. Het detecteren van de tijdzone tijdens het renderen zou hydratatie-mismatches veroorzaken, omdat server en client zich in verschillende tijdzones kunnen bevinden.
Voorbeelden#
Basis#
Een eenvoudige kalendercomponent. We hebben className="rounded border-2 shadow-md" gebruikt om de kalender op te maken.
"use client"
import { Calendar } from "@/components/ui/calendar"Bereikkalender#
Gebruik de prop mode="range" om selectie van een bereik in te schakelen.
"use client"
import * as React from "react"Maand- en jaarkiezer#
Gebruik captionLayout="dropdown" om dropdowns voor maand en jaar te tonen.
"use client"
import { Calendar } from "@/components/ui/calendar"Presets#
"use client"
import * as React from "react"Datum- en tijdkiezer#
"use client"
import * as React from "react"Geboekte datums#
"use client"
import * as React from "react"Aangepaste celgrootte#
"use client"
import * as React from "react"Je kunt de grootte van kalendercellen aanpassen met de CSS-variabele --cell-size. Je kunt hem ook responsief maken door breakpoint-specifieke waarden te gebruiken:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>Of gebruik vaste waarden:
<Calendar
mode="single"
selected={date}
onSelect={setDate}
className="rounded border-2 shadow-md [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>Weeknummers#
Gebruik showWeekNumber om weeknummers te tonen.
"use client"
import * as React from "react"RTL#
Om RTL-ondersteuning in Neobrutalism in te schakelen, zie de RTL-configuratiegids.
Zie ook de Hijri-gids om de Perzische / hijri / jalali-kalender in te schakelen.
"use client"
import * as React from "react"Wanneer je RTL gebruikt, importeer je de locale uit react-day-picker/locale en geef je de Calendar-component zowel de prop locale als de prop dir door:
import { arSA } from "react-day-picker/locale"
;<Calendar
mode="single"
selected={date}
onSelect={setDate}
locale={arSA}
dir="rtl"
/>Toegankelijkheid#
De kalender volgt het rasternavigatiemodel uit het WAI-ARIA Date Picker Dialog-patroon: React DayPicker rendert de maand als role="grid" met elke dag als echte knop, en kondigt geselecteerde en uitgeschakelde dagen aan bij schermlezers. De varianten voor beide backends delen dit gedrag, omdat ze dezelfde library wikkelen.
Toetsenbordinteracties:
| Toets | Actie |
|---|---|
Tab / Shift + Tab | Verplaatst de focus tussen de navigatieknoppen en het dagenraster |
ArrowLeft / ArrowRight | Verplaatst de focus naar de vorige / volgende dag |
ArrowUp / ArrowDown | Verplaatst de focus naar dezelfde weekdag in de vorige / volgende week |
Home / End | Verplaatst de focus naar de eerste / laatste dag van de week |
PageUp / PageDown | Gaat naar de vorige / volgende maand |
Shift + PageUp / Shift + PageDown | Gaat naar het vorige / volgende jaar |
Space / Enter | Selecteert de gefocuste dag |
API-referentie#
Zie de React DayPicker-documentatie voor meer informatie over de component Calendar.
Changelog#
RTL-ondersteuning#
Als je upgradet van een eerdere versie van de component Calendar, moet je de volgende updates toepassen om locale-ondersteuning toe te voegen:
Importeer het type Locale.
Voeg Locale toe aan je imports uit react-day-picker:
import {
DayPicker,
getDefaultClassNames,
type DayButton,
+ type Locale,
} from "react-day-picker"Voeg de prop locale toe aan de Calendar-component.
Voeg de prop locale toe aan de props van de component:
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
+ locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {Geef locale door aan DayPicker.
Geef de prop locale door aan de component 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,
}}Werk CalendarDayButton bij zodat het locale accepteert.
Werk de signatuur van de component CalendarDayButton bij en geef locale door:
function CalendarDayButton({
className,
day,
modifiers,
+ locale,
...props
- }: React.ComponentProps<typeof DayButton>) {
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {Werk de datumopmaak in CalendarDayButton bij.
Gebruik locale?.code in de datumopmaak:
<Button
variant="ghost"
size="icon"
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toLocaleDateString(locale?.code)}
...
/>Geef locale door aan de component DayButton.
Werk het gebruik van de component DayButton bij om de prop locale door te geven:
components={{
...
- DayButton: CalendarDayButton,
+ DayButton: ({ ...props }) => (
+ <CalendarDayButton locale={locale} {...props} />
+ ),
...
}}Werk RTL-bewuste CSS-klassen bij.
Vervang directionele klassen door logische eigenschappen voor betere RTL-ondersteuning:
// 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)Nadat je deze wijzigingen hebt toegepast, kun je de prop locale gebruiken om locale-specifieke opmaak te bieden:
import { enUS } from "react-day-picker/locale"
;<Calendar mode="single" selected={date} onSelect={setDate} locale={enUS} />