A lightweight, fully-typed React calendar component built for hotel bookings, travel apps, and date pickers.
Range selection Β· Single date Β· Hotel mode Β· Blocked dates Β· Min-nights Β· Smart suggestions Β· Mobile & desktop Β· SSR-safe
| Feature | Description |
|---|---|
| π― Single & Range Selection | Pick a single date or a check-in / check-out range |
| π¨ Hotel Mode | Allows checkout on blocked dates, just like real hotel calendars |
| π« Blocked Dates | Disable specific dates from selection |
| π Min Nights | Enforce per-date minimum-night requirements |
| π‘ Smart Suggestions | Built-in suggestion panel (e.g. "Republic Day Weekend") |
| π± Mobile & Desktop Variants | Optimized layouts with infinite scroll on mobile |
| π¨ Fully Customizable | Custom renderDay, renderMonthTitle, classNames, styles |
| π° Day Info Overlays | Show prices, labels, or badges below each date |
| π Calendar Events | Display event labels on date cells |
| βΏ Accessible | ARIA labels, keyboard nav, semantic HTML |
| β‘ Lazy Loading | Progressive month rendering for large date ranges |
| π SSR / Next.js Safe | Ships with "use client" directive β works out of the box with Next.js App Router |
| πͺΆ Zero Dependencies | Only react and react-dom as peer deps |
| π¦ TypeScript First | Full type definitions included |
# npm
npm install calendrix
# yarn
yarn add calendrix
# pnpm
pnpm add calendrixPeer dependencies:
react >= 16.8.0andreact-dom >= 16.8.0
import { Calendar } from "calendrix";
import "calendrix/styles.css";
function DatePicker() {
const [date, setDate] = useState<Date | null>(null);
return (
<Calendar
mode="single"
value={date}
onChange={(d) => setDate(d as Date | null)}
/>
);
}import { useState } from "react";
import { Calendar } from "calendrix";
import type { CalendarRange } from "calendrix";
import "calendrix/styles.css";
function BookingCalendar() {
const [range, setRange] = useState<CalendarRange>({ from: null, to: null });
return (
<Calendar
mode="range"
value={range}
onChange={(v) => setRange(v as CalendarRange)}
numberOfMonths={2}
variant="desktop"
showNavigation
/>
);
}A production-ready hotel calendar with blocked dates, minimum nights, prices, and smart suggestions:
import { useState } from "react";
import { Calendar } from "calendrix";
import type { CalendarRange, MinNights, DayInfo } from "calendrix";
import "calendrix/styles.css";
function HotelBookingCalendar() {
const [range, setRange] = useState<CalendarRange>({ from: null, to: null });
// Block specific dates
const blockedDates = ["2026-01-10", "2026-01-11", "2026-02-20"];
// Set minimum nights per check-in date
const minNights: MinNights = {
"2026-01-24": 3, // Republic Day weekend: 3-night minimum
"2026-02-14": 2, // Valentine's Day: 2-night minimum
};
// Show prices below dates
const dayInfo: DayInfo[] = [
{ date: "2026-01-16", text: "βΉ8K", textColor: "#0066cc" },
{ date: "2026-01-17", text: "βΉ9K", textColor: "#0066cc" },
{ date: "2026-01-24", text: "βΉ15K", textColor: "#cc0000" },
];
// Smart suggestion chips
const suggestions = [
{
label: "Jan 24β27",
sub: "Republic Day Weekend",
from: new Date(2026, 0, 24),
to: new Date(2026, 0, 27),
},
{
label: "Feb 14β16",
sub: "Valentine's Weekend",
from: new Date(2026, 1, 14),
to: new Date(2026, 1, 16),
},
];
return (
<Calendar
mode="range"
value={range}
onChange={(v) => setRange(v as CalendarRange)}
numberOfMonths={2}
variant="desktop"
calendarType="hotel"
blockedDates={blockedDates}
minNights={minNights}
dayInfo={dayInfo}
smartSuggestions={suggestions}
showSmartSuggestions
filterPastSuggestions
allowPastDates={false}
allowSameDay={false}
weekStartsOn={0}
labels={{ weekdayNamesShort: ["SU", "MO", "TU", "WE", "TH", "FR", "SA"] }}
/>
);
}Render many months at once with lazy-loaded rendering for smooth mobile scrolling:
<Calendar
mode="range"
value={range}
onChange={(v) => setRange(v as CalendarRange)}
numberOfMonths={12}
variant="mobile"
showNavigation={false}
initialMonthsToRender={3} // Render 3 first, rest load on scroll
allowPastDates={false}
calendarType="hotel"
/>Use renderDay to fully control how each date cell looks:
<Calendar
mode="single"
value={date}
onChange={(d) => setDate(d as Date | null)}
renderDay={({ state }) => (
<div style={{ textAlign: "center" }}>
<span>{state.date.getDate()}</span>
{state.today && <span>π</span>}
{state.dayInfo && (
<span style={{ color: state.dayInfo.textColor, fontSize: 10 }}>
{state.dayInfo.text}
</span>
)}
</div>
)}
/>The state object in renderDay gives you:
| Field | Type | Description |
|---|---|---|
date |
Date |
The date for this cell |
inMonth |
boolean |
Whether the date belongs to the displayed month |
disabled |
boolean |
Whether selection is disabled |
selected |
boolean |
Whether this date is selected |
inRange |
boolean |
Whether this date falls within the selected range |
rangeStart |
boolean |
Whether this is the start of a range |
rangeEnd |
boolean |
Whether this is the end of a range |
today |
boolean |
Whether this is today's date |
blockedByDate |
boolean |
Whether blocked via blockedDates |
blockedByMinNights |
boolean |
Whether blocked by minimum night rules |
eventLabels |
string[] |
Event names on this date |
dayInfo |
DayInfo | null |
Custom overlay info (price, label) |
minNightsRequired |
number | null |
Min-nights requirement if check-in here |
Add badges, icons, or holiday counts to month headers:
<Calendar
renderMonthTitle={(month, title) => (
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span>{title}</span>
<span style={{ fontSize: 11, color: "#e67e22" }}>3 Holidays</span>
</div>
)}
/>| Prop | Type | Default | Description |
|---|---|---|---|
mode |
"single" | "range" |
"single" |
Selection mode |
value |
Date | null | CalendarRange |
β | Controlled selected value |
defaultValue |
Date | null | CalendarRange |
β | Uncontrolled default value |
onChange |
(value) => void |
β | Selection change callback |
month |
Date |
β | Controlled visible month |
defaultMonth |
Date |
β | Default visible month |
onMonthChange |
(month: Date) => void |
β | Month navigation callback |
| Prop | Type | Default | Description |
|---|---|---|---|
numberOfMonths |
number |
1 |
Number of months to display |
variant |
"mobile" | "desktop" |
β | Layout variant |
showNavigation |
boolean |
true |
Show prev/next arrows |
weekStartsOn |
0β6 |
1 (Mon) |
First day of the week (0 = Sun) |
cellWidth |
number |
β | Cell width in px (desktop) |
cellHeight |
number |
β | Cell height in px (desktop) |
| Prop | Type | Default | Description |
|---|---|---|---|
minDate |
Date |
β | Earliest selectable date |
maxDate |
Date |
β | Latest selectable date |
isDateDisabled |
(date: Date) => boolean |
β | Custom disable logic |
blockedDates |
string[] |
β | Blocked dates ("YYYY-MM-DD") |
allowPastDates |
boolean |
false |
Allow selecting past dates |
allowSameDay |
boolean |
false |
Allow same check-in & checkout |
| Prop | Type | Default | Description |
|---|---|---|---|
calendarType |
"hotel" | null |
null |
Hotel mode (checkout on blocked dates) |
minNights |
{ [date: string]: number } |
β | Min-night rules per check-in date |
events |
CalendarEvent[] |
β | Event labels to display on dates |
showEvents |
boolean |
true |
Show/hide event labels |
dayInfo |
DayInfo[] |
β | Price / info overlays per date |
| Prop | Type | Default | Description |
|---|---|---|---|
smartSuggestions |
SmartSuggestion[] |
β | Suggestion items to display |
showSmartSuggestions |
boolean |
true |
Show/hide the suggestions panel |
filterPastSuggestions |
boolean |
true |
Auto-hide expired suggestions |
onSuggestionSelect |
(suggestion) => void |
β | Suggestion click callback |
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string |
β | Root element class |
style |
CSSProperties |
β | Root element inline styles |
classNames |
CalendarClassNames |
β | Class map for internal parts |
styles |
CalendarStyles |
β | Style map for internal parts |
labels |
CalendarLabels |
β | Custom month/weekday strings |
sidebar |
ReactNode |
β | Right-side content (desktop) |
footer |
ReactNode |
β | Bottom footer content |
renderDay |
(args) => ReactNode |
β | Custom day cell renderer |
renderMonthTitle |
(month, title) => ReactNode |
β | Custom month header renderer |
aria-label |
string |
β | Accessibility label |
| Prop | Type | Default | Description |
|---|---|---|---|
initialMonthsToRender |
number |
β | Months to render initially (rest lazy-load on scroll) |
All types are exported for full type safety:
import type {
CalendarProps,
CalendarValue,
CalendarRange,
CalendarSelectionMode,
CalendarDayState,
CalendarEvent,
CalendarType,
BlockedDates,
DayInfo,
MinNights,
SmartSuggestion,
} from "calendrix";Import the default styles:
import "calendrix/styles.css";Override styles using the classNames or styles props to target specific parts:
<Calendar
classNames={{
root: "my-calendar",
cell: "my-cell",
header: "my-header",
weekdays: "my-weekdays",
}}
styles={{
cell: { borderRadius: 8 },
}}
/>Targetable parts: root, shell, sidebar, months, month, header, title, nav, weekdays, weekday, grid, cell, footer
Calendrix ships with "use client" baked into the bundle β it works seamlessly with Next.js App Router without any extra wrappers.
// app/booking/page.tsx β works as-is, no "use client" needed in your file
import { Calendar } from "calendrix";
import "calendrix/styles.css";
export default function BookingPage() {
return <Calendar mode="range" numberOfMonths={2} />;
}Supports: React β₯ 16.8 Β· Next.js 12+ Β· Vite Β· CRA Β· Remix Β· Gatsby
calendrix/
βββ dist/
β βββ index.js # ESM bundle
β βββ index.cjs # CommonJS bundle
β βββ index.css # Default styles
β βββ index.d.ts # TypeScript declarations
β βββ index.d.cts # CTS declarations
βββ README.md
βββ LICENSE
# Clone and install
git clone https://github.com/bdbose/calendrix.git
cd calendrix
npm install
# Build the library
npm run build
# Run the demo app
cd examples/demo
npm install
npm run devBuilt with β€οΈ for the React community. Star β the repo if you find it useful!