Calendar
A month grid calendar with single date and range selection.
March 2026
Su
Mo
Tu
We
Th
Fr
Sa
import { Calendar } from "@/components/ui/calendar";
export function MyScreen() {
const [date, setDate] = useState<Date | undefined>();
return (
<Calendar
selected={date}
onSelect={setDate}
/>
);
}Installation
npx @aniui/cli add calendarUsage
app/index.tsx
import { Calendar } from "@/components/ui/calendar";
export function MyScreen() {
const [date, setDate] = useState<Date | undefined>();
return (
<Calendar
selected={date}
onSelect={setDate}
/>
);
}Range Selection
March 2026
Su
Mo
Tu
We
Th
Fr
Sa
Click to select start date
<Calendar
rangeStart={start}
rangeEnd={end}
onRangeChange={(s, e) => {
setStart(s);
setEnd(e);
}}
/>Props
PropTypeDefault
selectedDate—onSelect(date: Date) => void—rangeStartDate—rangeEndDate—onRangeChange(start: Date, end: Date | undefined) => void—minDate—maxDate—classNamestring—Source
components/ui/calendar.tsx
import React, { useState } from "react";
import { View, Text, Pressable } from "react-native";
import { cn } from "@/lib/utils";
export interface CalendarProps {
className?: string;
selected?: Date;
onSelect?: (date: Date) => void;
rangeStart?: Date;
rangeEnd?: Date;
onRangeChange?: (start: Date, end: Date | undefined) => void;
min?: Date;
max?: Date;
}
const DAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
const same = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
export function Calendar({ className, selected, onSelect, rangeStart, rangeEnd, onRangeChange, min, max }: CalendarProps) {
const [viewing, setViewing] = useState(() => selected ?? rangeStart ?? new Date());
const year = viewing.getFullYear(), month = viewing.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
const cells: (number | null)[] = [...Array(firstDay).fill(null), ...Array.from({ length: daysInMonth }, (_, i) => i + 1)];
const label = new Date(year, month).toLocaleString("default", { month: "long", year: "numeric" });
const handlePress = (day: number) => {
const date = new Date(year, month, day);
if ((min && date < min) || (max && date > max)) return;
if (onRangeChange) {
if (!rangeStart || rangeEnd || date < rangeStart) onRangeChange(date, undefined);
else onRangeChange(rangeStart, date);
}
onSelect?.(date);
};
return (
<View className={cn("rounded-lg bg-background p-3", className)}>
<View className="flex-row items-center justify-between mb-3">
<Pressable onPress={() => setViewing(new Date(year, month - 1, 1))} className="h-9 w-9 items-center justify-center rounded-md" accessibilityRole="button" accessibilityLabel="Previous month">
<Text className="text-base text-muted-foreground">{"‹"}</Text>
</Pressable>
<Text className="text-sm font-semibold text-foreground">{label}</Text>
<Pressable onPress={() => setViewing(new Date(year, month + 1, 1))} className="h-9 w-9 items-center justify-center rounded-md" accessibilityRole="button" accessibilityLabel="Next month">
<Text className="text-base text-muted-foreground">{"›"}</Text>
</Pressable>
</View>
<View className="flex-row mb-1">
{DAYS.map((d) => <View key={d} className="flex-1 items-center py-1"><Text className="text-xs font-medium text-muted-foreground">{d}</Text></View>)}
</View>
<View className="flex-row flex-wrap">
{cells.map((day, i) => {
if (day === null) return <View key={`e-${i}`} className="w-[14.28%] h-9" />;
const date = new Date(year, month, day);
const sel = selected && same(date, selected);
const rs = rangeStart && same(date, rangeStart);
const re = rangeEnd && same(date, rangeEnd);
const inR = rangeStart && rangeEnd && date.getTime() >= rangeStart.getTime() && date.getTime() <= rangeEnd.getTime();
const today = same(date, new Date());
const off = (min && date < min) || (max && date > max);
return (
<View key={day} className="w-[14.28%] items-center">
<Pressable onPress={() => handlePress(day)} disabled={!!off} className={cn("h-9 w-9 items-center justify-center rounded-full", sel || rs || re ? "bg-primary" : inR ? "bg-accent" : "", today && !sel && "border border-primary", off && "opacity-30")} accessibilityRole="button" accessibilityLabel={`${label} ${day}`}>
<Text className={cn("text-sm", sel || rs || re ? "text-primary-foreground font-semibold" : "text-foreground")}>{day}</Text>
</Pressable>
</View>
);
})}
</View>
</View>
);
}