Calendar
A date field component that allows users to enter and edit date.
The Calendar component displays a date picker with month navigation, day selection, and visual indicators for the selected date. It shows dates from the current month along with dates from adjacent months. Built from scratch using React and native HTML elements. No UI library dependencies.
Code
TypeScript: Copy this code into components/ui/calendar.tsx:
tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface CalendarProps {
value?: Date
defaultValue?: Date
onValueChange?: (date: Date | undefined) => void
className?: string
}
const DAYS_OF_WEEK = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
const MONTHS = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
]
function getDaysInMonth(year: number, month: number): number {
return new Date(year, month + 1, 0).getDate()
}
function getFirstDayOfMonth(year: number, month: number): number {
return new Date(year, month, 1).getDay()
}
function getDaysForMonth(year: number, month: number): (number | null)[] {
const daysInMonth = getDaysInMonth(year, month)
const firstDay = getFirstDayOfMonth(year, month)
const days: (number | null)[] = []
const prevMonthDays = getDaysInMonth(year, month - 1)
for (let i = firstDay - 1; i >= 0; i--) {
days.push(prevMonthDays - i)
}
for (let i = 1; i <= daysInMonth; i++) {
days.push(i)
}
const remaining = 42 - days.length
for (let i = 1; i <= remaining; i++) {
days.push(i)
}
return days
}
export function Calendar({ value, defaultValue, onValueChange, className }: CalendarProps) {
const [selectedDate, setSelectedDate] = React.useState<Date | undefined>(
value ?? defaultValue
)
const [currentMonth, setCurrentMonth] = React.useState(() => {
const date = value ?? defaultValue ?? new Date()
return date.getMonth()
})
const [currentYear, setCurrentYear] = React.useState(() => {
const date = value ?? defaultValue ?? new Date()
return date.getFullYear()
})
React.useEffect(() => {
if (value !== undefined) {
setSelectedDate(value)
setCurrentMonth(value.getMonth())
setCurrentYear(value.getFullYear())
}
}, [value])
const handleDateClick = (day: number, isCurrentMonth: boolean) => {
if (!isCurrentMonth) return
const newDate = new Date(currentYear, currentMonth, day)
setSelectedDate(newDate)
onValueChange?.(newDate)
}
const handlePrevMonth = () => {
if (currentMonth === 0) {
setCurrentMonth(11)
setCurrentYear(currentYear - 1)
} else {
setCurrentMonth(currentMonth - 1)
}
}
const handleNextMonth = () => {
if (currentMonth === 11) {
setCurrentMonth(0)
setCurrentYear(currentYear + 1)
} else {
setCurrentMonth(currentMonth + 1)
}
}
const days = getDaysForMonth(currentYear, currentMonth)
const firstDay = getFirstDayOfMonth(currentYear, currentMonth)
const daysInMonth = getDaysInMonth(currentYear, currentMonth)
return (
<div
className={cn(
"rounded-lg border-2 border-foreground bg-primary text-primary-foreground p-4 neobrutalism-shadow",
className
)}
>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<button
onClick={handlePrevMonth}
className="flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-primary hover:bg-primary/90 transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none"
aria-label="Previous month"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<h3 className="font-bold text-lg">
{MONTHS[currentMonth]} {currentYear}
</h3>
<button
onClick={handleNextMonth}
className="flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-primary hover:bg-primary/90 transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none"
aria-label="Next month"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
</div>
{/* Day labels */}
<div className="grid grid-cols-7 gap-1 mb-2">
{DAYS_OF_WEEK.map((day) => (
<div key={day} className="text-center text-sm font-bold py-1">
{day}
</div>
))}
</div>
{/* Date grid */}
<div className="grid grid-cols-7 gap-1">
{days.map((day, index) => {
const isPrevMonth = index < firstDay
const isNextMonth = index >= firstDay + daysInMonth
const isCurrentMonth = !isPrevMonth && !isNextMonth
const isSelected =
isCurrentMonth &&
selectedDate &&
selectedDate.getDate() === day &&
selectedDate.getMonth() === currentMonth &&
selectedDate.getFullYear() === currentYear
return (
<button
key={index}
onClick={() => day !== null && handleDateClick(day, isCurrentMonth)}
disabled={!isCurrentMonth}
className={cn(
"flex h-9 w-9 items-center justify-center rounded border-2 border-foreground text-sm font-bold transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none",
isCurrentMonth
? "bg-primary hover:bg-primary/90 cursor-pointer"
: "bg-primary/50 text-primary-foreground/50 cursor-not-allowed",
isSelected && "bg-foreground text-background"
)}
>
{day}
</button>
)
})}
</div>
</div>
)
}JavaScript: Copy this code into components/ui/calendar.jsx:
jsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const DAYS_OF_WEEK = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
const MONTHS = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
]
function getDaysInMonth(year, month) {
return new Date(year, month + 1, 0).getDate()
}
function getFirstDayOfMonth(year, month) {
return new Date(year, month, 1).getDay()
}
function getDaysForMonth(year, month) {
const daysInMonth = getDaysInMonth(year, month)
const firstDay = getFirstDayOfMonth(year, month)
const days = []
const prevMonthDays = getDaysInMonth(year, month - 1)
for (let i = firstDay - 1; i >= 0; i--) {
days.push(prevMonthDays - i)
}
for (let i = 1; i <= daysInMonth; i++) {
days.push(i)
}
const remaining = 42 - days.length
for (let i = 1; i <= remaining; i++) {
days.push(i)
}
return days
}
export function Calendar({ value, defaultValue, onValueChange, className }) {
const [selectedDate, setSelectedDate] = React.useState(
value ?? defaultValue
)
const [currentMonth, setCurrentMonth] = React.useState(() => {
const date = value ?? defaultValue ?? new Date()
return date.getMonth()
})
const [currentYear, setCurrentYear] = React.useState(() => {
const date = value ?? defaultValue ?? new Date()
return date.getFullYear()
})
React.useEffect(() => {
if (value !== undefined) {
setSelectedDate(value)
setCurrentMonth(value.getMonth())
setCurrentYear(value.getFullYear())
}
}, [value])
const handleDateClick = (day, isCurrentMonth) => {
if (!isCurrentMonth) return
const newDate = new Date(currentYear, currentMonth, day)
setSelectedDate(newDate)
onValueChange?.(newDate)
}
const handlePrevMonth = () => {
if (currentMonth === 0) {
setCurrentMonth(11)
setCurrentYear(currentYear - 1)
} else {
setCurrentMonth(currentMonth - 1)
}
}
const handleNextMonth = () => {
if (currentMonth === 11) {
setCurrentMonth(0)
setCurrentYear(currentYear + 1)
} else {
setCurrentMonth(currentMonth + 1)
}
}
const days = getDaysForMonth(currentYear, currentMonth)
const firstDay = getFirstDayOfMonth(currentYear, currentMonth)
const daysInMonth = getDaysInMonth(currentYear, currentMonth)
return (
<div
className={cn(
"rounded-lg border-2 border-foreground bg-primary text-primary-foreground p-4 neobrutalism-shadow",
className
)}
>
{/* Header */}
<div className="flex items-center justify-between mb-4">
<button
onClick={handlePrevMonth}
className="flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-primary hover:bg-primary/90 transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none"
aria-label="Previous month"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
</button>
<h3 className="font-bold text-lg">
{MONTHS[currentMonth]} {currentYear}
</h3>
<button
onClick={handleNextMonth}
className="flex h-8 w-8 items-center justify-center rounded border-2 border-foreground bg-primary hover:bg-primary/90 transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none"
aria-label="Next month"
>
<svg
className="h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
</button>
</div>
{/* Day labels */}
<div className="grid grid-cols-7 gap-1 mb-2">
{DAYS_OF_WEEK.map((day) => (
<div key={day} className="text-center text-sm font-bold py-1">
{day}
</div>
))}
</div>
{/* Date grid */}
<div className="grid grid-cols-7 gap-1">
{days.map((day, index) => {
const isPrevMonth = index < firstDay
const isNextMonth = index >= firstDay + daysInMonth
const isCurrentMonth = !isPrevMonth && !isNextMonth
const isSelected =
isCurrentMonth &&
selectedDate &&
selectedDate.getDate() === day &&
selectedDate.getMonth() === currentMonth &&
selectedDate.getFullYear() === currentYear
return (
<button
key={index}
onClick={() => day !== null && handleDateClick(day, isCurrentMonth)}
disabled={!isCurrentMonth}
className={cn(
"flex h-9 w-9 items-center justify-center rounded border-2 border-foreground text-sm font-bold transition-colors neobrutalism-shadow-sm active:translate-x-[1px] active:translate-y-[1px] active:shadow-none",
isCurrentMonth
? "bg-primary hover:bg-primary/90 cursor-pointer"
: "bg-primary/50 text-primary-foreground/50 cursor-not-allowed",
isSelected && "bg-foreground text-background"
)}
>
{day}
</button>
)
})}
</div>
</div>
)
}Usage
TypeScript:
tsx
import { Calendar } from "@/components/ui/calendar"
function MyComponent() {
const [date, setDate] = React.useState<Date | undefined>(new Date())
return (
<Calendar
value={date}
onValueChange={setDate}
/>
)
}JavaScript:
jsx
import { Calendar } from "@/components/ui/calendar"
import { useState } from "react"
function MyComponent() {
const [date, setDate] = useState(undefined)
return (
<Calendar
value={date}
onValueChange={setDate}
/>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.
Examples
December 2025
Su
Mo
Tu
We
Th
Fr
Sa