Skip to content

PlCalendar

A month, on the page rather than in a popup. The same grid a PlDatePicker opens, with the trigger and the popup taken away.

React
tsx
import { PlCalendar } from 'plass-ui';

<PlCalendar value={day} onValueChange={setDay} />;
dart
import 'package:plass_ui/plass_ui.dart';

PlCalendar(
  value: day,
  onChanged: (DateTime? next) => setState(() => day = next),
);

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the sheet is made of. ghost when it is already inside something that draws one
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'Cell, radius and type scale together. There is no density
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'The family the chosen day, the today marker and the focus ring take
elevationshared0 | 1 | 2 | 31Drop shadow depth. 0 means no shadow at all
valueDate | nullThe chosen day. Use with onValueChange for a controlled calendar
defaultValueDate | nullThe day it starts on, for an uncontrolled one
onValueChange(value: Date | null) => voidCalled when a day is chosen
precision'day' | 'month' | 'year''day'The smallest unit it hands back. A floor rather than a starting view, and the value is normalised to the start of it
monthDateThe month on screen. Use with onMonthChange to control it
defaultMonthDateThe month it opens on. Defaults to the value's, or this month
onMonthChange(month: Date) => voidCalled when the month on screen changes
minDateDate | nullNothing before this day can be chosen. Read at the calendar's precision
maxDateDate | nullNothing after it can be chosen. Read at the calendar's precision
shouldDisableDate(date: Date) => booleanBlocks individual days — weekends, holidays, a booked date. Day-granular, so month and year never consult it
localestringThe BCP 47 tag the month names, weekday initials and first day of the week come from. The page's own by default
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 6Which day the week starts on, as Date counts them — Sunday is 0. Worked out from locale when absent
showOutsideDaysbooleantrueDraws the leading and trailing days belonging to the neighbouring months
autoFocusbooleanfalseTakes the focus on mount. Off, because a calendar in a page is not a popup
disabledbooleanfalseGreys the whole calendar and takes it out of the tab order with inert. There is no readOnly
namestringSubmits with a form. The spelling follows precision — YYYY-MM-DD, YYYY-MM, YYYY
labelsPartial<PlPickerLabels>The strings Intl has no opinion about — the buttons and the headings
PropTypeDefaultDescription
value * DateTime?The chosen day, or null for none. Controlled, like every other input in the package
onChangedValueChanged<DateTime?>?Called with the day that was taken. A null onChanged makes it inert
precisionPlCalendarPrecisionPlCalendarPrecision.dayThe smallest unit it hands back. A floor rather than a starting view, and the value is normalised to the start of it
monthDateTime?The month on screen. Use with onMonthChange to control it
defaultMonthDateTime?The month it opens on. Defaults to the value's, or this month
onMonthChangedValueChanged<DateTime>?Called when the month on screen changes
minDateDateTime?Nothing before this day can be chosen. Read at the calendar's precision
maxDateDateTime?Nothing after it can be chosen. Read at the calendar's precision
shouldDisableDatebool Function(DateTime)?Blocks individual days — weekends, holidays, a booked date. Day-granular, so month and year never consult it
weekStartsOnPlassWeekday?Which day the week starts on, as Date counts them — Sunday is 0. Worked out from locale when absent
namesPlDateNamesPlDateNames.englishThe words the calendar draws — the months, the weekdays. This is what a locale string is in the React build
labelsPlPickerLabelsPlPickerLabels.englishThe words it says about itself — the steppers, the headings
showOutsideDaysbooltrueDraws the leading and trailing days belonging to the neighbouring months
autofocusboolfalseTakes the focus on mount. Off, because a calendar in a page is not a popup
disabledboolfalseGreys the whole calendar and takes it out of the tab order with inert. There is no readOnly
variantsharedPlassVariantPlassVariant.glassWhat the sheet is made of. ghost when it is already inside something that draws one
sizesharedPlassSizePlassSize.mdCell, radius and type scale together. There is no density
colorsharedPlassColorPlassColor.primaryThe family the chosen day, the today marker and the focus ring take
elevationsharedint1Drop shadow depth. 0 means no shadow at all
semanticLabelString?The name a screen reader gives the whole grid

Every native <div> attribute passes straight through. There is no label, description or error: this is not a field, so it has no text around it. Put it in a PlFieldset if it needs a caption.

There is no label, description or error: this is not a field, so it has no text around it. Put it in a PlFieldset if it needs a caption.

Two differences from the React build, both the ones every date component in this package has. names and labels take the words rather than a locale string, because the framework ships no Intl. English is the default, and an app that already has package:intl builds a PlDateNames from it in three lines. And there is no name: a Dart form is not an HTML one, so there is no hidden input to submit and the value is the caller's to send.

density is not offered. Padding on a grid of forty-two squares is what makes them stop being squares; size moves the whole ladder together instead. What the shared axes mean across the library is in prop conventions.

PlCalendar or PlDatePicker

A PlDatePicker is a field that happens to open a calendar: it belongs in a form, beside other fields, and its answer is one line of text until you open it. This is a calendar that is not standing in for a field, a booking page, an availability view, a date rail in a dashboard. The grid is the interface rather than a way of filling one in.

If the answer sits in a form next to other inputs, use the picker.

Examples

precision

A floor rather than a starting view. At month the month grid is the last grid and pressing a cell in it answers, so there is no day grid under it at all. A card's expiry is a month, and a control that made somebody answer which day of December 2027 is one that will be answered wrongly.

The value is normalised to the start of what was chosen: the 1st of the month, the 1st of January, never whichever day the cursor was resting on.

React

minDate, maxDate and shouldDisableDate

The two bounds are read at the calendar's own precision, so a minDate of 15 July leaves July pickable on a month calendar. shouldDisableDate is day-granular and is not consulted at all on the other two.

React

variant

glass by default, with the sheet and the elevation a PlCard has. Reach for ghost when the calendar is already inside something that draws a sheet. A second bordered rectangle inside the first is a second rectangle.

React

Controlling the month

month and onMonthChange control what is on screen independently of what is chosen, which is what two calendars kept a month apart need.

tsx
const [month, setMonth] = useState(startOfMonth(new Date()));

<PlCalendar month={month} onMonthChange={setMonth} value={day} onValueChange={setDay} />;

Uncontrolled, the month follows the value: choosing a day out of a trailing week moves the grid to that day's month, because a selection in a month that is no longer on screen is one nobody can see.

In a form

name adds a hidden input. The spelling follows precision, YYYY-MM-DD, then YYYY-MM and YYYY, which is what the native inputs of the same shape submit.

tsx
<form action={book}>
  <PlCalendar name="departure" />
</form>

There is nothing to add. A Dart form is not an HTML one, so there is no hidden input and no name to give it. The value arrives in onChanged and sending it is the caller's.

disabled

Greys the calendar and takes it out of reach with the inert attribute, one attribute rather than a disabled on forty-two cells.

There is no readOnly beside it, and that is not an omission: a read-only field still shows a value a reader can select and copy, and a calendar has nothing to copy. To block some days rather than all of them, use shouldDisableDate.

Accessibility

  • A real role="grid" with one roving tab stop, so Tab leaves the calendar rather than walking forty-two cells. That is the pattern the ARIA date-picker practice describes, and the reason no cell is a disabled button. A blocked day is aria-disabled and still reachable, so a keyboard reader can find out that it is blocked.
  • The arrow keys move by one cell, PageUp/PageDown by a month (a year with Shift), Home/End to the ends of the week. Running off an edge steps the calendar rather than stopping.
  • Each cell's accessible name is the full date in the calendar's locale, so a screen reader reads "Monday 27 July 2026" rather than "27".
  • autoFocus is off by default, the opposite of the picker's: a popup has just been opened by somebody who wants to be in it, and a calendar in a page has not.

Released under the MIT License