Calendar
A month of days, for picking one, several, or a range.
A month of days, for picking one, several, or a span with two ends.
It carries no date library. Everything a month grid needs is arithmetic on the platform's own Date, and shipping a timezone database to render seven columns is a poor trade in a library whose entire runtime is three small packages. Month and weekday names come from Intl, guarded — builds without the full locale data fall back to English rather than taking the screen down over a month name.
Installation
Calendar ships with the library — no separate install.
import { Calendar, Card, Text } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add calendarUsage
const [day, setDay] = useState<Date>();
<Calendar selected={day} onSelect={setDay} />
// A span, which takes two taps to complete.
const [range, setRange] = useState<DateRange>();
<Calendar mode="range" selected={range} onSelect={setRange} />Composition
<Calendar>
<Calendar.Grid>…</Calendar.Grid>
<Calendar.Day>…</Calendar.Day>
</Calendar>Calendar.Grid— One month: the weekday headings and six rows of days. Rendered for you pernumberOfMonths; exported for a layout that needs the grid without the caption.Calendar.Day— One cell — the number, the disc when it is selected, and the band when it is inside a range.
Examples
A single day
Tapping the selected day again clears it, so there is a way back to nothing without a separate control.
const [day, setDay] = useState<Date>();
<Calendar selected={day} onSelect={setDay} />A range
Two taps. A third starts again, and a tap that lands before the open end becomes the new start rather than being refused — someone who picked the wrong month first should not have to undo before retrying.
const [range, setRange] = useState<DateRange>();
<Calendar mode="range" selected={range} onSelect={setRange} />Several days
multiple collects a set. Tapping a day already in it takes it out.
const [days, setDays] = useState<Date[]>([]);
<Calendar mode="multiple" selected={days} onSelect={setDays} />Days ruled out
disabled takes a list, a span, or a rule. A rule, because "no weekends" and "nothing in the past" are the two common cases and neither can be written as a list that is still right tomorrow.
<Calendar
selected={day}
onSelect={setDay}
minDate={new Date()}
disabled={(date) => date.getDay() === 0 || date.getDay() === 6}
/>Month and year pickers
captionLayout="dropdown" is the difference between choosing a birthday in four taps and in four hundred.
<Calendar
selected={day}
onSelect={setDay}
captionLayout="dropdown"
maxDate={new Date()}
defaultMonth={new Date(1996, 5, 1)}
/>Two months at once
What picking a range across a month boundary wants. The arrows split across the row — back on the first caption, forward on the last — so there is one way to page rather than two.
<Calendar mode="range" numberOfMonths={2} selected={range} onSelect={setRange} />Hijri or Gregorian
calendar moves the grid, not only the labels — a Hijri month starts on a different day and runs 29 or 30. The value stays a plain Date either way, so the choice is a presentation one and nothing downstream has to know which was on screen.
<Calendar selected={day} onSelect={setDay} calendar="islamic" />
{/* Or follow the device, for an app whose language and calendar are the
same question. */}
<Calendar selected={day} onSelect={setDay} calendar="auto" />API Reference
Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
mode | Mode | 'single' as Mode | One day, several, or a span with two ends. |
selected | CalendarSelection[Mode] | — | Controlled selection. Its shape follows mode. |
defaultSelected | CalendarSelection[Mode] | — | Starting selection when uncontrolled. |
onSelect | (selected: CalendarSelection[Mode]) => void | — | |
month | Date | — | Controlled visible month. Any day inside it will do. |
defaultMonth | Date | — | Month shown first when uncontrolled. Defaults to the selection, or today. |
onMonthChange | (month: Date) => void | — | |
numberOfMonths | number | 1 | Months side by side. Two is what picking a range across a boundary wants. |
captionLayout | CalendarCaptionLayout | 'label' | dropdown turns the caption into month and year pickers, which is the difference between choosing a birthday in four taps and in four hundred. |
disabled | CalendarDisabled | — | Days that cannot be picked: a list, a span, or a rule. |
minDate | Date | — | Earliest selectable day. Also stops the caption paging back past it. |
maxDate | Date | — | Latest selectable day. |
weekStartsOn | number | 0 | 0 is Sunday. |
showOutsideDays | boolean | true | Draw the neighbouring months' days rather than leaving the cells blank. |
locale | DateLocale | — | BCP 47 tag for the month and weekday names. The device's own by default. |
calendar | CalendarSystem | 'gregory' | Which calendar the months and the day numbers are counted in. gregory by default rather than auto: a grid whose month boundaries move with the device's language is a surprise, and it is the caller — not the locale — who knows whether the dates being picked are Hijri ones. auto follows the locale for the cases where they are the same question. |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
The grid is always six weeks. A month needs five rows or six depending on the day it starts on, and drawing only the rows it uses makes the calendar change height as it is paged — everything below jumps, and the days themselves appear to shift between months. The spare cells hold the neighbouring months' days, or are left blank with showOutsideDays={false}. Blank, not absent: the row has to stay seven wide or the columns stop lining up with their headings.
A range is drawn in three pieces. On the web the band under a range is one background with its ends rounded by :first-child and :last-child. There are no pseudo-classes here, so each day works out whether it is a start, a middle or an end — and, separately, whether it is at the edge of its own row, because a range running over a weekend has to close off on Saturday and open again on Sunday rather than trailing into the gap. The band is drawn behind the number rather than as the cell's own background, which is what lets it run from cell to cell.
Days are compared at local midnight. A Date carries a time, and two values on the same day are not equal until it is stripped.
It opens on the selection, not on today. A picker reopened on a date chosen last year lands on that month — otherwise it opens on today and the choice looks lost.
minDate and maxDate bound the paging too, not only the selection: an arrow that pages to a month where every day is dead is an arrow that should have been off.
calendar goes all the way down. It is gregory by default rather than auto, because a grid whose month boundaries move with the device's language is a surprise — and it is the caller, not the locale, who knows whether the dates being picked are Hijri ones. Set islamic and the month anchoring, the paging, the outside-day dimming and the day numbers all count in it, so the caption and the cells can never disagree. Month lengths are asked of the platform's own calendar data a day at a time rather than read from a table, which is what keeps a 29-day month 29 days in the years it is one.
Digits are pinned to Latin. The cells render numbers, so a caption in Arabic-Indic over cells in Latin would be two scripts for one date; both are Latin instead.
Without islamic data in the build, it falls back to Gregorian rather than throwing — wrong, but coherent, which is the same posture the month names already take.