Calendar
The Calendar component provides a flexible date picker with support for single date selection, date ranges, and multiple date selection. Built with accessibility in mind and full keyboard navigation support.
Live Examples
View Code
View Code
View Code
Import
import { Calendar } from "@jarllyng/nostromo";
// or
import { Calendar } from "@jarllyng/nostromo/calendar";Usage
import { Calendar } from "@jarllyng/nostromo";
export default function Example() {
return (
<Calendar mode="single" label="Select Date" placeholder="Choose a date" />
);
}Features
Selection Modes
- Single - Select one date
- Range - Select a date range (from/to)
- Multiple - Select multiple individual dates
Date Constraints
- Min/Max dates - Restrict selectable date range
- Disabled dates - Disable specific dates
- Disabled days - Disable specific weekdays (e.g., weekends)
Customization
- Locale support - Customize date formatting
- First day of week - Sunday or Monday
- Show outside days - Display days from previous/next month
- Multiple months - Display multiple months side-by-side
Robust Date Handling
- date-fns integration - Uses
date-fnslibrary for reliable date manipulation - Leap year support - Correctly handles leap years and edge cases
- Timezone aware - Proper timezone handling for date operations
- Localization ready - Built-in support for different locales
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
mode | "single" | "range" | "multiple" | "single" | No | Selection mode |
value | Date | Date[] | { from?: Date; to?: Date } | undefined | No | Controlled value |
defaultValue | Date | Date[] | { from?: Date; to?: Date } | undefined | No | Uncontrolled default value |
onChange | (value: Date | Date[] | { from?: Date; to?: Date } | undefined) => void | undefined | No | Callback when value changes |
minDate | Date | undefined | No | Minimum selectable date |
maxDate | Date | undefined | No | Maximum selectable date |
disabledDates | Date[] | undefined | No | Array of disabled dates |
disabledDays | number[] | undefined | No | Array of disabled weekdays (0=Sun, 1=Mon, etc.) |
locale | string | "en-US" | No | Locale for date formatting |
placeholder | string | "Select date..." | No | Input placeholder text |
label | string | undefined | No | Input label |
error | boolean | false | No | Error state |
helperText | string | undefined | No | Helper text below input |
className | string | undefined | No | Additional CSS classes |
inputClassName | string | undefined | No | Additional CSS classes for input |
calendarClassName | string | undefined | No | Additional CSS classes for calendar popover |
variant | "default" | "outline" | "ghost" | "default" | No | Calendar visual variant |
size | "sm" | "md" | "lg" | "md" | No | Calendar size |
showOutsideDays | boolean | true | No | Show days from previous/next month |
numberOfMonths | number | 1 | No | Number of months to display |
firstDayOfWeek | 0 | 1 | 1 | No | First day of week (0=Sunday, 1=Monday) |
labels | CalendarLabels | English defaults | No | The words the calendar says itself |
Examples
Single Date Selection
<Calendar
mode="single"
label="Select Date"
placeholder="Choose a date"
onChange={(date) => console.log("Selected:", date)}
/>Date Range Selection
<Calendar
mode="range"
label="Select Date Range"
placeholder="Choose start and end date"
onChange={(range) => console.log("Range:", range)}
/>Multiple Date Selection
<Calendar
mode="multiple"
label="Select Multiple Dates"
placeholder="Choose multiple dates"
onChange={(dates) => console.log("Selected dates:", dates)}
/>With Date Constraints
<Calendar
mode="single"
label="Select Future Date"
minDate={new Date()}
maxDate={new Date(2025, 11, 31)}
disabledDays={[0, 6]} // Disable weekends
/>With Disabled Dates
const holidays = [
new Date(2025, 0, 1), // New Year's Day
new Date(2025, 6, 4), // Independence Day
new Date(2025, 11, 25) // Christmas
]
<Calendar
mode="single"
label="Select Date"
disabledDates={holidays}
/>Controlled Component
const [selectedDate, setSelectedDate] = useState<Date | undefined>();
<Calendar
mode="single"
value={selectedDate}
onChange={setSelectedDate}
label="Controlled Calendar"
/>;Accessibility
- ✅ WCAG 2.1 AA compliant
- ✅ Keyboard navigation - Full keyboard support with arrow keys
- ✅ ARIA labels - Proper labels for all interactive elements
- ✅ Screen reader support - Announcements for date selection with full date format
- ✅ Focus management - Proper focus handling in popover with visual focus indicators
- ✅ Date announcements - Clear date format announcements (weekday, year, month, day)
- ✅ Role attributes - Proper ARIA roles (application, button, selected states)
- ✅ Disabled state - Proper aria-disabled attributes for disabled dates
Keyboard Navigation
- Arrow Right/Left - Navigate to next/previous day
- Arrow Up/Down - Navigate to same day next/previous week
- Enter/Space - Select focused date
- Escape - Close calendar popover
- Tab - Navigate between controls
- Home - Navigate to first day of month
- End - Navigate to last day of month
- Page Up - Navigate to previous month (same day)
- Page Down - Navigate to next month (same day)
Do’s and Don’ts
✅ Do
- Use appropriate mode for your use case
- Provide clear labels and helper text
- Set min/max dates when applicable
- Use disabledDays for business rules (e.g., weekends)
- Provide error feedback when validation fails
❌ Don’t
- Use range mode for single date selection
- Forget to handle empty states
- Use without labels for accessibility
- Disable too many dates (hurts UX)
- Use multiple mode for date ranges
Localisation
locale moves every date: the value on the trigger, the weekday headings, the
month heading and the day labels a screen reader reads. It is passed to Intl, so
any locale tag the runtime knows works and nothing has to be imported.
<Calendar locale="da-DK" value={new Date(2026, 8, 14)} />
// 14. sep. 2026, over søn. man. tirs. ons. tors. fre. lør.A locale cannot translate “Today”. Those words are labels:
<Calendar
locale="da-DK"
labels={{
openCalendar: "Åbn kalender",
placeholder: "Vælg dato...",
previousMonth: "Forrige måned",
nextMonth: "Næste måned",
today: "I dag",
selectEndDate: "Vælg slutdato",
multipleSelected: (count) => `${count} datoer valgt`,
}}
/>Anything you leave out keeps its English default, and the placeholder prop still
wins over labels.placeholder.
Hiding the outside days keeps the columns
showOutsideDays={false} hides the dates from the neighbouring months, and the
cells they occupied stay behind as empty spacers. They are what puts the 1st of
the month under the right weekday.
The spacers are aria-hidden and are not controls, so a screen reader and the Tab
key both skip them.
Keyboard
Arrow keys move a day or a week, Home and End go to the ends of the month, Page Up and Page Down move a month, Enter and Space select, Escape closes.
Every one of them lands on a date that can actually be focused. A movement that would land on a disabled date keeps going in the same direction until it finds one that is not, and if nothing in that direction is enabled, focus stays where it is rather than moving to a control that cannot hold it.
A month step keeps the day number where the target month has one: Page Down from 31 March is 30 April, not 1 May.
Opening the calendar puts focus on the selected date, or on today, or on the first enabled day of the month shown, in that order. The month shown is the month of the selected date.