All components
Calendar
Date picker grid.
stable
Preview
August 2026
Su
Mo
Tu
We
Th
Fr
Sa
Code
tsx
import { Calendar } from "@/components/ui/calendar";
export function Example() {
return <Calendar />;
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | Date | — | Controlled selected date. |
| defaultValue | Date | — | Uncontrolled initial date. |
| onValueChange | (date: Date | null) => void | — | Called when selection changes. |
| minDate | Date | — | Earliest selectable date. |
| maxDate | Date | — | Latest selectable date. |
| className | string | — | Additional Tailwind classes, merged with the component's defaults via cn(). |
Pure React (no external date library). Month navigation only.
All components also accept standard HTML attributes for their root element (e.g. `id`, `aria-*`, `data-*`, event handlers) and forward `ref` where applicable.
Source
The full implementation of components/ui/calendar.tsx is below — copy it into your project and own it. Some components import the cn helper from lib/utils or other primitives; copy those too.
tsx
"use client";
import { buttonVariants } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useState } from "react";
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const MONTHS = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
function startOfMonth(year: number, month: number) {
return new Date(year, month, 1).getDay();
}
function daysInMonth(year: number, month: number) {
return new Date(year, month + 1, 0).getDate();
}
function isSameDay(a: Date, b: Date) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
export interface CalendarProps {
className?: string;
/** Controlled selected date. Pair with `onSelect`. */
selected?: Date;
/** Initial selection in uncontrolled mode. */
defaultSelected?: Date;
onSelect?: (date: Date) => void;
disabled?: (date: Date) => boolean;
defaultMonth?: Date;
}
export function Calendar({
className,
selected,
defaultSelected,
onSelect,
disabled,
defaultMonth,
}: CalendarProps) {
const today = new Date();
const isControlled = selected !== undefined;
const [internalSelected, setInternalSelected] = useState<Date | undefined>(defaultSelected);
const activeSelected = isControlled ? selected : internalSelected;
const [viewDate, setViewDate] = useState(defaultMonth ?? activeSelected ?? today);
const year = viewDate.getFullYear();
const month = viewDate.getMonth();
const firstDay = startOfMonth(year, month);
const totalDays = daysInMonth(year, month);
const prevMonth = () => setViewDate(new Date(year, month - 1, 1));
const nextMonth = () => setViewDate(new Date(year, month + 1, 1));
const cells: (number | null)[] = [
...Array(firstDay).fill(null),
...Array.from({ length: totalDays }, (_, i) => i + 1),
];
// Pad to full rows
while (cells.length % 7 !== 0) cells.push(null);
return (
<div className={cn("p-3 w-fit select-none", className)}>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<button
type="button"
onClick={prevMonth}
className={cn(buttonVariants({ variant: "ghost", size: "icon" }), "h-7 w-7")}
aria-label="Previous month"
>
<ChevronLeft className="h-4 w-4" />
</button>
<span className="text-sm font-medium text-fg">
{MONTHS[month]} {year}
</span>
<button
type="button"
onClick={nextMonth}
className={cn(buttonVariants({ variant: "ghost", size: "icon" }), "h-7 w-7")}
aria-label="Next month"
>
<ChevronRight className="h-4 w-4" />
</button>
</div>
{/* Day names */}
<div className="grid grid-cols-7 mb-1">
{DAYS.map((d) => (
<div
key={d}
className="flex h-8 items-center justify-center text-xs font-medium text-fg-muted"
>
{d}
</div>
))}
</div>
{/* Day cells */}
<div className="grid grid-cols-7 gap-y-0.5">
{cells.map((day, idx) => {
if (day === null) return <div key={`empty-${idx}`} />;
const date = new Date(year, month, day);
const isToday = isSameDay(date, today);
const isSelected = activeSelected ? isSameDay(date, activeSelected) : false;
const isDisabled = disabled?.(date) ?? false;
const handleClick = () => {
if (!isControlled) setInternalSelected(date);
onSelect?.(date);
};
return (
<button
key={day}
type="button"
disabled={isDisabled}
onClick={handleClick}
aria-selected={isSelected}
aria-label={date.toLocaleDateString()}
className={cn(
"flex h-8 w-8 items-center justify-center rounded-md text-sm transition-colors",
"focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-accent",
"disabled:pointer-events-none disabled:opacity-30",
isSelected && "bg-accent text-accent-fg font-semibold",
!isSelected && isToday && "border border-accent text-accent font-semibold",
!isSelected && !isToday && "text-fg hover:bg-bg-muted",
)}
>
{day}
</button>
);
})}
</div>
</div>
);
}JavaFX
Ships in the PRIZM JavaFX library for thick-client C3 apps as PrizmCalendar (extends VBox). Run the gallery to see it natively.
java
import design.prizm.fx.controls.PrizmCalendar;
PrizmCalendar()| Prop | Type | Default | Description |
|---|---|---|---|
| setSelected | (LocalDate) → void | — | Set the selected date (also moves to its month). |
| getSelected | () → LocalDate | — | The selected date, or null. |
| setOnSelect | (Consumer<LocalDate>) → void | — | Called when a day is picked. |
A custom month grid (header nav + day-name row + 7-col day cells; today outlined, selected filled). JavaFX has no standalone month grid. Mirrors components/ui/calendar.tsx.