Text and fields
Calendar
Pick a date or a range from a month that stays on screen.
November 2026
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 |
import { Calendar } from "macbetter/calendar"
export default function Example() {
return <Calendar aria-label="Delivery date" defaultValue="2026-11-20" />
}When to use it
Use a calendar when the month itself helps: choosing a delivery day, seeing which days are free, picking a reporting period. It stays on the page; for a field that opens a calendar only when asked, use a Date Picker.
Today is marked in accent text. The chosen day fills with the accent. Days that can't be picked are dimmed, and stay focusable so keyboard users hear why.
Usage
import { Calendar } from "macbetter/calendar"Values are ISO strings like "2026-11-20". With no children, Calendar draws its header and a grid for each month. To change one part, write the parts out: CalendarHeader, CalendarHeading, CalendarPrevious, CalendarNext and CalendarGrid, which draws its own weekday row and days.
Ranges and two months
mode="range" picks a start and an end. Moving the pointer previews the range, and the days between the two circles join into one continuous band that rounds off where a week wraps. months={2} shows two months side by side.
November – December 2026
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 |
| Sun | Mon | Tue | Wed | Thu | Fri | Sat |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | ||
| 6 | 7 | 8 | 9 | 10 | 11 | 12 |
| 13 | 14 | 15 | 16 | 17 | 18 | 19 |
| 20 | 21 | 22 | 23 | 24 | 25 | 26 |
| 27 | 28 | 29 | 30 | 31 |
import { Calendar } from "macbetter/calendar"
export default function Example() {
return (
<Calendar
aria-label="Report period"
mode="range"
months={2}
defaultValue={["2026-11-09", "2026-11-19"]}
/>
)
}Compact
size="sm" is the dense month of a macOS popover or sidebar, with single-letter weekdays. unavailable takes dates, ranges, weekdays or a function.
November 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 |
import { Calendar } from "macbetter/calendar"
export default function Example() {
return <Calendar size="sm" aria-label="Go to date" defaultValue="2026-11-18" unavailable={{ weekday: [0, 6] }} />
}Platform behavior
- macOS. 13-point days in 26-point circles, a 13-point semibold month title, and small gray chevrons that highlight under the pointer.
- iPadOS and iOS. 20-point days in 40-point circles, a 17-point semibold title, uppercase weekday abbreviations, and accent chevrons with 44-point touch targets, as in the iOS graphical date picker.
- Every platform. Pressing a day gives a small, instant squeeze. Days from neighboring months are hidden unless you pass
showOutsideDays.
Accessibility
- The month is a
gridnamed by its title, and the title is announced as it changes. - Arrow keys move by day and week, Home and End to the week's ends, Page Up and Page Down by month. Enter or Space picks the focused day; Escape cancels an unfinished range.
- Previous and Next are named "Previous month" and "Next month"; pass
aria-labelin another language.
API
Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "md" | "md" | Compact popover month, or the full graphical picker. |
mode | "single" | "multiple" | "range" | "single" | One date, any number, or a start and an end. |
value | string | string[] | [string, string] | null | — | The selection, for a controlled calendar. |
defaultValue | string | string[] | [string, string] | null | — | The selection it starts with. |
onValueChange | (value, details) => void | — | Runs when a day is picked, or a range's end. |
months | number | 1 | How many months show side by side. |
min | string | — | The earliest date that can be picked. |
max | string | — | The latest date that can be picked. |
unavailable | DateMatchers | — | Dates that can't be picked. |
weekdayFormat | "narrow" | "short" | "long" | "short", or "narrow" at sm | How weekday headers are written. |
showOutsideDays | boolean | false | Shows the neighboring months' days at the edges. |
today | string | — | Today's date, when it isn't the device's. |
CalendarHeader takes the props of a div. CalendarGrid takes an offset for the month it shows. CalendarDay draws its number in a circle unless it has children.
Data attributes
| Attribute | On | Values |
|---|---|---|
data-size | Calendar | sm, md |
data-selected | Day | Present on picked days; in a range, every day from start to end |
data-today | Day | Present on today |
data-range-start, data-range-middle, data-range-end | Day | Where the day sits in a range |
data-preview | Day | Present on days of a range being picked |
data-unavailable, data-disabled | Day | Can't be picked |
CSS variables
| Variable | macOS | iOS | What it sets |
|---|---|---|---|
--mb-calendar-cell-width | 32px | 46px | Column width |
--mb-calendar-cell-height | 30px | 44px | Row height |
--mb-calendar-circle | 26px | 40px | Day circle |
--mb-calendar-day-font | 13px | 20px | Day numbers |
--mb-calendar-band | Accent tint | Accent tint | A range's band, set on the root |