Calendar
A date field component that allows users to enter and edit dates.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Calendar is a purely server-rendered month grid. Each cell is a data-ui="dp-day" button with a data-date="yyyy-mm-dd". The ui.js island intercepts the click and bubbles the selection up to the parent (DatePicker or a custom form field).
Three axes of control:
- Displayed month:
month="yyyy-mm". Default: the month ofvalue, or January 2026 as a fallback. The prev/next nav emitsdata-ui="dp-nav"with the newdata-month; the island rebuilds the body viarebuildCalendar().captionLayout="dropdown"(default) renders two native<select>elements, month and year, laid over the label withopacity-0. UsecaptionLayout="label"for a static title instead. - Selection:
value="yyyy-mm-dd"for a single date, orvalue + valueEndfor a range (the days in between are highlighted withbg-muted). Two<Calendar>side by side sharing the samevalue/valueEndpair form a two-month range picker. - Locale:
"fr"(default) or"en". Affects the weekday labels (lu ma me je ve sa divsSu Mo Tu We Th Fr Sa) and the month names.weekStart={0}starts the week on Sunday (default: Monday).
Additional options: minDate / maxDate (ISO bounds; days outside them are disabled and shown at 50% opacity); showOutsideDays={false} to skip rendering the days of the previous/next month (default: rendered in gray).
Installation
$ bext ui add calendar
Copy and paste the following into src/components/ui/calendar.tsx.
This component is interactive. Make sure the shared island is loaded (see Islands). It reacts to data-ui="calendar" hooks.
Usage
Anatomy
<Calendar>
├── nav (absolute inset-x-0 top-0) // prev/next arrows
│ ├── <button data-ui="dp-nav" data-month="prev-yyyy-mm">‹</button>
│ └── <button data-ui="dp-nav" data-month="next-yyyy-mm">›</button>
└── month_caption // month title
├── captionLayout="dropdown" (default)
│ ├── <select data-ui="dp-select-month"> // month (opacity-0)
│ └── <select data-ui="dp-select-year"> // year (opacity-0)
└── captionLayout="label"
└── <div data-ui="dp-label">Juin 2026</div>
<table> (border-collapse) // the grid
├── <thead><tr>weekdays (lu ma me je ve sa di)</tr>
└── <tbody data-ui="dp-grid">
└── 6 <tr class="mt-2 flex w-full">
└── 7 <td>
└── <button data-ui="dp-day" data-date="yyyy-mm-dd">N</button>API Reference
Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Selected date in ISO "yyyy-mm-dd" format. |
| valueEnd | string | — | End of the range in ISO format. When set together with value, the days in between are highlighted with bg-muted. |
| month | string | month of value, or "2026-01" | Displayed month in "yyyy-mm" format. Sets the initial state; the user can navigate elsewhere through the island. |
| locale | "fr" | "en" | "fr" | Locale for the weekday labels (lu ma me je ve sa di / Su Mo Tu We Th Fr Sa) and month names. |
| weekStart | 0 | 1 | 1 | First day of the week. 0 = Sunday (US), 1 = Monday (FR/ISO). |
| captionLayout | "dropdown" | "label" | "dropdown" | "dropdown" (default): two <select> elements, month and year, as an overlay. "label": static text. |
| fromYear | number | year - 100 | Lowest year in the year dropdown. |
| toYear | number | year + 10 | Highest year in the year dropdown. |
| minDate | string | — | Lower ISO bound. Earlier days are disabled and shown at opacity-50. |
| maxDate | string | — | Upper ISO bound. Later days are disabled. |
| showOutsideDays | boolean | true | Render the days of the previous/next month in gray (they can be clicked to navigate to the adjacent month). |
| className | string | — | Additional CSS classes on the root. Typically "rounded-lg border shadow-sm" when used standalone (outside a popover). |
Data Attributes
Attributes you can target in custom CSS to override the style for each state.
| Attribute | On | Values | Description |
|---|---|---|---|
| data-ui="dp-calendar" | root | — | Marker used to find the Calendar from the island. data-month="yyyy-mm" reflects the displayed month. |
| data-ui="dp-nav" | prev/next buttons | data-month="yyyy-mm" | Clicking triggers rebuildCalendar() for the given month, without a reload. |
| data-ui="dp-select-month/year" | <select> | 0..11 / year | On change → rebuildCalendar for the chosen month/year. |
| data-ui="dp-day" | every day | data-date="yyyy-mm-dd" | Clicking triggers the selection. Bubbled to the parent DatePicker if there is one. |
| data-outside="1" | dp-day buttons | present on outside days | Day of the adjacent month (grayed out). Clicking navigates to that month, then selects. |
| data-selected="true" | <td> | when the day is selected | Used by the shadcn CSS selectors for rounded corners in a range. |
Keyboard
| Key | Action |
|---|---|
| Tab | Focus order: prev nav / month dropdown / year dropdown / next nav / grid. |
| Enter / Space | On a prev/next button: navigates. On a day button: selects. |
| Arrow keys | On an open month/year <select>: native navigation. |
Accessibility
Calendar uses a real <table> with a <thead> (weekdays) and a <tbody> (dates). Screen readers announce the structure naturally. Weekdays are in <th> cells and each date in a <td>.
The prev/next navigation buttons carry an aria-label that follows the locale: "Previous month" / "Next month" with locale="en", "Mois précédent" / "Mois suivant" with locale="fr". The two dropdowns are labelled "Month" / "Year" the same way. The month and year dropdowns are real native <select> elements (keyboard and AT accessible without custom JS), styled with opacity-0 over a visible label.
Disabled days (outside the minDate/maxDate bounds) carry disabled aria-disabled="true". Outside days (adjacent month) remain focusable: clicking one navigates to that month before selecting.
For a full composable date picker, wrap it in a <DatePicker>, which adds the trigger and a hidden input for form submission (see DatePicker).
Examples
Range (two months side by side)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Without adjacent-month days
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
French locale, week starting on Monday
| lu | ma | me | je | ve | sa | di |
|---|---|---|---|---|---|---|
minDate / maxDate bounds
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Static caption (no dropdowns)
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.
On This Page