PRIZM 4.0DSTA
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

PropTypeDefaultDescription
valueDateControlled selected date.
defaultValueDateUncontrolled initial date.
onValueChange(date: Date | null) => voidCalled when selection changes.
minDateDateEarliest selectable date.
maxDateDateLatest selectable date.
classNamestringAdditional 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()
PropTypeDefaultDescription
setSelected(LocalDate) → voidSet the selected date (also moves to its month).
getSelected() → LocalDateThe selected date, or null.
setOnSelect(Consumer<LocalDate>) → voidCalled 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.