bx

Calendar

A date field component that allows users to enter and edit dates.

June
2026
SuMoTuWeThFrSa
tsx
<Calendar
  month="2026-06"
  value="2026-06-13"
  locale="en"
  className="rounded-lg border shadow-sm"
/>

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 of value, or January 2026 as a fallback. The prev/next nav emits data-ui="dp-nav" with the new data-month; the island rebuilds the body via rebuildCalendar(). captionLayout="dropdown" (default) renders two native <select> elements, month and year, laid over the label with opacity-0. Use captionLayout="label" for a static title instead.
  • Selection: value="yyyy-mm-dd" for a single date, or value + valueEnd for a range (the days in between are highlighted with bg-muted). Two <Calendar> side by side sharing the same value/valueEnd pair form a two-month range picker.
  • Locale: "fr" (default) or "en". Affects the weekday labels (lu ma me je ve sa di vs Su 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

Terminal
$ bext ui add calendar

Usage

tsx
import { Calendar } from "@bext-stack/ui/primitives"

// Single date, explicit displayed month, selected day:
<Calendar
  month="2026-06"
  value="2026-06-13"
  locale="en"
  className="rounded-lg border"
/>

// Range (2 months side by side): share value + valueEnd across two instances.
<div className="rounded-lg border inline-flex">
  <Calendar month="2026-01" value="2026-01-12" valueEnd="2026-02-11" />
  <Calendar month="2026-02" value="2026-01-12" valueEnd="2026-02-11" />
</div>

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)

January
2026
SuMoTuWeThFrSa
February
2026
SuMoTuWeThFrSa
tsx
// Two <Calendar> sharing the same value/valueEnd pair. The range is
// highlighted between the two days across both months.
<div className="rounded-lg border bg-background p-0 inline-flex flex-col gap-0 sm:flex-row">
  <Calendar month="2026-01" value="2026-01-12" valueEnd="2026-02-11" locale="en" />
  <Calendar month="2026-02" value="2026-01-12" valueEnd="2026-02-11" locale="en" />
</div>

Without adjacent-month days

August
2026
SuMoTuWeThFrSa
tsx
<Calendar
  month="2026-08"
  value="2026-08-05"
  locale="en"
  showOutsideDays={false}
  className="rounded-lg border"
/>

French locale, week starting on Monday

juin
2026
lumamejevesadi
tsx
<Calendar
  month="2026-06"
  value="2026-06-13"
  locale="fr"
  weekStart={1}
  className="rounded-lg border"
/>

minDate / maxDate bounds

June
2026
SuMoTuWeThFrSa
tsx
// Days outside the bounds are disabled (opacity-50 + pointer-events-none).
<Calendar
  month="2026-06"
  value="2026-06-13"
  locale="en"
  minDate="2026-06-05"
  maxDate="2026-06-25"
  className="rounded-lg border"
/>

Static caption (no dropdowns)

June 2026
SuMoTuWeThFrSa
tsx
// captionLayout="label": the month title is plain text instead of
// the two month + year <select> elements.
<Calendar month="2026-06" value="2026-06-13" locale="en" captionLayout="label" className="rounded-lg border" />
Try it live
Open in play

Edit this component live in the bext playground. The PRISM + signals version is the bext-native idiom: fine-grained reactivity, no virtual DOM.

src/app/page.tsx 2 files · signals · runs in your browser
Pure PRISM + real bext signals: fine-grained, no re-render.