İçeriğe atla

Takvim

Rezervasyonlar, planlama ve aralık seçimi için aylık ızgaralı tarih seçici — neobrütalist tarzda kenarlık ve gölge efektleriyle.

"use client"

import * as React from "react"

Takvim, tek bir tarih, birden çok tarih ya da bir aralık seçmek için bir ay ızgarası işler. React DayPicker üzerine kuruludur — iki arka uç varyantı da aynı kütüphaneyi sarar — ve neobrütalist reçeteye göre biçimlendirilmiştir: kalın kenarlıklar, sert gölgeler ve koyu tipografi.

Şu durumlarda tercih edin:

  • Form tarih alanları — doğum günleri, son tarihler ve teslimat tarihleri için bir popover ile eşleştirip bir Date Picker oluşturun.
  • Rezervasyon akışları — giriş/çıkış tarihleri için mode="range", dolu günler için disabled.
  • Pano filtreleri — rapor tarih aralıkları, uygunluk görünümleri ve “son 7 gün” gibi hazır kısayollar.

Kurulum

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/calendar.json
npx shadcn@latest add https://neobrutalism.com/r/base/calendar.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/calendar.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/calendar.json

Şu bağımlılıkları kurun:

pnpm add react-day-picker date-fns
npm install react-day-picker date-fns
yarn add react-day-picker date-fns
bun add react-day-picker date-fns

Projenize Button bileşenini ekleyin.

Calendar bileşeni Button bileşenini kullanır. Projenizde kurulu olduğundan emin olun.

Aşağıdaki kodu kopyalayıp projenize yapıştırın.

components/ui/calendar.tsx
"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 }

Import yollarını projenizin yapısına uyacak şekilde güncelleyin.

Kullanım

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"
  />
)

Daha fazla bilgi için React DayPicker dokümanlarına bakın.

Hakkında

Calendar bileşeni React DayPicker üzerine kuruludur.

Date Picker

Bir tarih seçici oluşturmak için <Calendar> bileşenini kullanabilirsiniz. Daha fazla bilgi için Date Picker sayfasına bakın.

Fars / Hicri / Celali Takvimi

Fars takvimini kullanmak için components/ui/calendar.tsx dosyasını düzenleyin ve react-day-picker yerine react-day-picker/persian yazın.

- import { DayPicker } from "react-day-picker"
+ import { DayPicker } from "react-day-picker/persian"
"use client"

import * as React from "react"

Seçili Tarih (Saat Dilimiyle)

Calendar bileşeni, tarihlerin kullanıcının yerel saat diliminde gösterilip seçilmesini sağlamak için bir timeZone prop'u kabul eder.

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}
    />
  )
}

Not: Seçili tarihte bir kayma fark ederseniz (örneğin, 20'yi seçmek 19'u vurguluyorsa), timeZone prop'unun kullanıcının yerel saat dilimine ayarlı olduğundan emin olun.

Neden istemci tarafında? Saat dilimi, sunucu tarafı render ile uyumluluğu sağlamak amacıyla bir useEffect içinde Intl.DateTimeFormat().resolvedOptions().timeZone kullanılarak algılanır. Saat dilimini render sırasında algılamak, sunucu ve istemci farklı saat dilimlerinde olabileceğinden hydration uyuşmazlıklarına yol açar.

Örnekler

Temel

Temel bir takvim bileşeni. Takvimi biçimlendirmek için className="rounded border-2 shadow-md" kullandık.

"use client"

import { Calendar } from "@/components/ui/calendar"

Aralık Takvimi

Aralık seçimini etkinleştirmek için mode="range" prop'unu kullanın.

"use client"

import * as React from "react"

Ay ve Yıl Seçici

Ay ve yıl için dropdown'lar göstermek amacıyla captionLayout="dropdown" kullanın.

"use client"

import { Calendar } from "@/components/ui/calendar"

Hazır Ayarlar

"use client"

import * as React from "react"

Tarih ve Saat Seçici

"use client"

import * as React from "react"

Rezerve edilmiş tarihler

"use client"

import * as React from "react"

Özel Hücre Boyutu

"use client"

import * as React from "react"

Takvim hücrelerinin boyutunu --cell-size CSS değişkenini kullanarak özelleştirebilirsiniz. Ayrıca kırılma noktasına özel değerler kullanarak duyarlı hâle de getirebilirsiniz:

<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
  className="rounded border-2 shadow-md [--cell-size:--spacing(11)] md:[--cell-size:--spacing(12)]"
/>

Veya sabit değerler kullanın:

<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
  className="rounded border-2 shadow-md [--cell-size:2.75rem] md:[--cell-size:3rem]"
/>

Hafta Numaraları

Hafta numaralarını göstermek için showWeekNumber kullanın.

"use client"

import * as React from "react"

RTL

Neobrutalism'de RTL desteğini etkinleştirmek için RTL yapılandırma kılavuzuna bakın.

Fars / Hicri / Celali takvimini etkinleştirmek için ayrıca Hicri kılavuzuna bakın.

"use client"

import * as React from "react"

RTL kullanırken, locale'i react-day-picker/locale'den import edin ve Calendar bileşenine hem locale hem de dir prop'larını geçirin:

import { arSA } from "react-day-picker/locale"
 
;<Calendar
  mode="single"
  selected={date}
  onSelect={setDate}
  locale={arSA}
  dir="rtl"
/>

Erişilebilirlik

Takvim, WAI-ARIA Date Picker Dialog deseninin ızgara gezinme modelini izler: React DayPicker ayı role="grid" olarak, her günü ise gerçek bir düğme olarak işler ve seçili ile devre dışı günleri ekran okuyuculara duyurur. Aynı kütüphaneyi sardıkları için iki arka uç varyantı da bu davranışı paylaşır.

Klavye etkileşimleri:

TuşEylem
Tab / Shift + TabOdağı gezinme düğmeleri ile gün ızgarası arasında taşır
ArrowLeft / ArrowRightOdağı önceki / sonraki güne taşır
ArrowUp / ArrowDownOdağı önceki / sonraki haftanın aynı gününe taşır
Home / EndOdağı haftanın ilk / son gününe taşır
PageUp / PageDownÖnceki / sonraki aya geçer
Shift + PageUp / Shift + PageDownÖnceki / sonraki yıla geçer
Space / EnterOdaklanılan günü seçer

API Referansı

Calendar bileşeni hakkında daha fazla bilgi için React DayPicker dokümanlarına bakın.

Değişiklik Günlüğü

RTL Desteği

Calendar bileşeninin önceki bir sürümünden yükseltiyorsanız, locale desteği eklemek için aşağıdaki güncellemeleri uygulamanız gerekir:

Locale tipini import edin.

react-day-picker'dan yaptığınız import'lara Locale'i ekleyin:

  import {
    DayPicker,
    getDefaultClassNames,
    type DayButton,
+   type Locale,
  } from "react-day-picker"

Calendar bileşenine locale prop'unu ekleyin.

locale prop'unu bileşenin prop'larına ekleyin:

  function Calendar({
    className,
    classNames,
    showOutsideDays = true,
    captionLayout = "label",
    buttonVariant = "ghost",
+   locale,
    formatters,
    components,
    ...props
  }: React.ComponentProps<typeof DayPicker> & {
    buttonVariant?: React.ComponentProps<typeof Button>["variant"]
  }) {

locale'i DayPicker'a geçirin.

locale prop'unu DayPicker bileşenine geçirin:

    <DayPicker
      showOutsideDays={showOutsideDays}
      className={cn(...)}
      captionLayout={captionLayout}
+     locale={locale}
      formatters={{
        formatMonthDropdown: (date) =>
-         date.toLocaleString("default", { month: "short" }),
+         date.toLocaleString(locale?.code, { month: "short" }),
        ...formatters,
      }}

CalendarDayButton'ı locale kabul edecek şekilde güncelleyin.

CalendarDayButton bileşeninin imzasını güncelleyin ve locale'i geçirin:

  function CalendarDayButton({
    className,
    day,
    modifiers,
+   locale,
    ...props
- }: React.ComponentProps<typeof DayButton>) {
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {

CalendarDayButton'daki tarih biçimlendirmesini güncelleyin.

Tarih biçimlendirmesinde locale?.code kullanın:

    <Button
      variant="ghost"
      size="icon"
-     data-day={day.date.toLocaleDateString()}
+     data-day={day.date.toLocaleDateString(locale?.code)}
      ...
    />

locale'i DayButton bileşenine geçirin.

locale prop'unu geçirmek için DayButton bileşeninin kullanımını güncelleyin:

      components={{
        ...
-       DayButton: CalendarDayButton,
+       DayButton: ({ ...props }) => (
+         <CalendarDayButton locale={locale} {...props} />
+       ),
        ...
      }}

RTL uyumlu CSS sınıflarını güncelleyin.

Daha iyi RTL desteği için yönlü sınıfları mantıksal özelliklerle değiştirin:

  // 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)

Bu değişiklikleri uyguladıktan sonra, locale'e özgü biçimlendirme sağlamak için locale prop'unu kullanabilirsiniz:

import { enUS } from "react-day-picker/locale"
 
;<Calendar mode="single" selected={date} onSelect={setDate} locale={enUS} />