Layr
⌘K
Ctrl K
Design Systems
AI Designer
Account
Roadmap
Sign in
Adobe Spectrum - RangeCalendar
Getting started
Adobe Spectrum
Spectrum UI
Pro
Components
Autocomplete
Breadcrumbs
Button
Calendar
Checkbox
Checkbox Group
Color Area
Color Field
Color Picker
Color Slider
Color Swatch
Color Swatch Picker
Color Wheel
Combo Box
Date Field
Date Picker
Date Range Picker
Dialog
Disclosure
Disclosure Group
Drop Zone
File Trigger
Form
Grid List
Group
Link
List Box
Menu
Meter
Modal
Number Field
Popover
Progress Bar
Radio Group
Range Calendar
Search Field
Select
Slider
Switch
Table
Tabs
Tag Group
Text Field
Time Field
Toast
Toggle Button
Toggle Button Group
Toolbar
Tooltip
Tree
Virtualizer
Advanced
React Aria Components
Styling
{}, onFocusChange = () => {}, isDisabled = false, isReadOnly = false, isInvalid = false, allowsNonContiguousRanges = false, firstDayOfWeek = null, isDateUnavailable = () => false, errorMessage = '', visibleDuration = { months: 1 }, minValue = null, maxValue = null } = options; const calendar = document.createElement('div'); calendar.className = 'react-aria-RangeCalendar'; calendar.setAttribute('aria-label', ariaLabel); if (isDisabled) calendar.setAttribute('aria-disabled', 'true'); if (isReadOnly) calendar.setAttribute('aria-readonly', 'true'); if (isInvalid) calendar.setAttribute('aria-invalid', 'true'); // Current state let currentValue = value; let currentFocusedDate = value?.start || today(); let currentMonth = currentFocusedDate; let isSelecting = false; let selectionStart = null; // Header const header = document.createElement('header'); const prevButton = document.createElement('button'); prevButton.className = 'react-aria-Button'; prevButton.innerHTML = ChevronLeftIcon; prevButton.setAttribute('aria-label', 'Previous month'); prevButton.disabled = isDisabled || isReadOnly; const heading = document.createElement('h2'); heading.className = 'react-aria-Heading'; const nextButton = document.createElement('button'); nextButton.className = 'react-aria-Button'; nextButton.innerHTML = ChevronRightIcon; nextButton.setAttribute('aria-label', 'Next month'); nextButton.disabled = isDisabled || isReadOnly; header.appendChild(prevButton); header.appendChild(heading); header.appendChild(nextButton); // Calendar grid container const gridContainer = document.createElement('div'); if (visibleDuration.months > 1) { gridContainer.className = 'multi-month-container'; } // Error message const errorDiv = document.createElement('div'); errorDiv.className = 'error-message'; errorDiv.style.display = errorMessage ? 'block' : 'none'; errorDiv.textContent = errorMessage; calendar.appendChild(header); calendar.appendChild(gridContainer); calendar.appendChild(errorDiv); // Update heading text function updateHeading() { const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; heading.textContent = monthNames[currentMonth.month - 1] + ' ' + currentMonth.year; } // Create calendar grid for a specific month function createCalendarGrid(monthOffset = 0) { const gridMonth = addDays(currentMonth, monthOffset * 30); // Approximate month offset const grid = document.createElement('div'); grid.className = 'react-aria-CalendarGrid'; const table = document.createElement('table'); const thead = document.createElement('thead'); const tbody = document.createElement('tbody'); // Header row with weekday names const headerRow = document.createElement('tr'); const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; // Adjust weekdays based on firstDayOfWeek let adjustedWeekdays = [...weekdays]; if (firstDayOfWeek === 'mon') { adjustedWeekdays = [...weekdays.slice(1), weekdays[0]]; } adjustedWeekdays.forEach(day => { const th = document.createElement('th'); th.className = 'react-aria-CalendarHeaderCell'; th.textContent = day; headerRow.appendChild(th); }); thead.appendChild(headerRow); // Generate calendar days const firstDay = new Date(gridMonth.year, gridMonth.month - 1, 1); const lastDay = new Date(gridMonth.year, gridMonth.month, 0); const startDate = new Date(firstDay); // Adjust start date to show previous month days if needed let dayOffset = firstDay.getDay(); if (firstDayOfWeek === 'mon') { dayOffset = (dayOffset + 6) % 7; } startDate.setDate(startDate.getDate() - dayOffset); // Create 6 weeks of days for (let week = 0; week < 6; week++) { const row = document.createElement('tr'); for (let day = 0; day < 7; day++) { const cellDate = new Date(startDate); cellDate.setDate(startDate.getDate() + (week * 7) + day); const cell = document.createElement('td'); const button = document.createElement('button'); button.className = 'react-aria-CalendarCell'; button.textContent = cellDate.getDate(); const dateObj = createCalendarDate( cellDate.getFullYear(), cellDate.getMonth() + 1, cellDate.getDate() ); // Set data attributes if (cellDate.getMonth() !== gridMonth.month - 1) { button.setAttribute('data-outside-month', 'true'); } if (isDisabled) { button.disabled = true; button.setAttribute('data-disabled', 'true'); } if (isDateUnavailable(dateObj)) { button.setAttribute('data-unavailable', 'true'); button.disabled = true; } // Check if date is in selected range if (currentValue && currentValue.start && currentValue.end) { const cellTime = cellDate.getTime(); const startTime = new Date(currentValue.start.year, currentValue.start.month - 1, currentValue.start.day).getTime(); const endTime = new Date(currentValue.end.year, currentValue.end.month - 1, currentValue.end.day).getTime(); if (cellTime >= startTime && cellTime <= endTime) { button.setAttribute('data-selected', 'true'); if (cellTime === startTime) { button.setAttribute('data-selection-start', 'true'); } if (cellTime === endTime) { button.setAttribute('data-selection-end', 'true'); } } } // Event handlers if (!isDisabled && !isReadOnly && !isDateUnavailable(dateObj)) { button.addEventListener('click', () => { if (!isSelecting) { // Start selection selectionStart = dateObj; isSelecting = true; currentValue = { start: dateObj, end: dateObj }; } else { // End selection const start = selectionStart; const end = dateObj; // Ensure start is before end const startTime = new Date(start.year, start.month - 1, start.day).getTime(); const endTime = new Date(end.year, end.month - 1, end.day).getTime(); if (startTime <= endTime) { currentValue = { start, end }; } else { currentValue = { start: end, end: start }; } isSelecting = false; selectionStart = null; onChange(currentValue); } updateCalendar(); }); button.addEventListener('mouseenter', () => { if (isSelecting && selectionStart) { // Preview selection const start = selectionStart; const end = dateObj; const startTime = new Date(start.year, start.month - 1, start.day).getTime(); const endTime = new Date(end.year, end.month - 1, end.day).getTime(); if (startTime <= endTime) { currentValue = { start, end }; } else { currentValue = { start: end, end: start }; } updateCalendar(); } }); } cell.appendChild(button); row.appendChild(cell); } tbody.appendChild(row); } table.appendChild(thead); table.appendChild(tbody); grid.appendChild(table); return grid; } // Update calendar display function updateCalendar() { updateHeading(); // Clear existing grids gridContainer.innerHTML = ''; // Create grids for visible months for (let i = 0; i < visibleDuration.months; i++) { const grid = createCalendarGrid(i); gridContainer.appendChild(grid); } // Update error message errorDiv.textContent = errorMessage; errorDiv.style.display = errorMessage ? 'block' : 'none'; } // Navigation handlers prevButton.addEventListener('click', () => { if (!isDisabled && !isReadOnly) { currentMonth = createCalendarDate(currentMonth.year, currentMonth.month - 1, 1); if (currentMonth.month === 0) { currentMonth = createCalendarDate(currentMonth.year - 1, 12, 1); } updateCalendar(); onFocusChange(currentMonth); } }); nextButton.addEventListener('click', () => { if (!isDisabled && !isReadOnly) { currentMonth = createCalendarDate(currentMonth.year, currentMonth.month + 1, 1); if (currentMonth.month === 13) { currentMonth = createCalendarDate(currentMonth.year + 1, 1, 1); } updateCalendar(); onFocusChange(currentMonth); } }); // Public methods calendar.setValue = (newValue) => { currentValue = newValue; updateCalendar(); }; calendar.setDisabled = (disabled) => { prevButton.disabled = disabled; nextButton.disabled = disabled; if (disabled) { calendar.setAttribute('aria-disabled', 'true'); } else { calendar.removeAttribute('aria-disabled'); } updateCalendar(); }; calendar.setReadOnly = (readonly) => { if (readonly) { calendar.setAttribute('aria-readonly', 'true'); } else { calendar.removeAttribute('aria-readonly'); } updateCalendar(); }; calendar.setInvalid = (invalid) => { if (invalid) { calendar.setAttribute('aria-invalid', 'true'); } else { calendar.removeAttribute('aria-invalid'); } }; calendar.setErrorMessage = (message) => { errorDiv.textContent = message; errorDiv.style.display = message ? 'block' : 'none'; }; // Initial render updateCalendar(); return calendar; } // Examples // Basic example const basicCalendar = createRangeCalendar({ ariaLabel: 'Trip dates', onChange: (value) => console.log('Basic calendar changed:', value) }); document.getElementById('basic-example').appendChild(basicCalendar); // Interactive demo let demoValue = null; const valueDisplay = document.getElementById('value-display'); function updateValueDisplay() { valueDisplay.innerHTML = '
Selected range:
' + formatDateRange(demoValue); } const interactiveCalendar = createRangeCalendar({ ariaLabel: 'Interactive demo', value: demoValue, onChange: (value) => { demoValue = value; updateValueDisplay(); } }); document.getElementById('interactive-demo').appendChild(interactiveCalendar); updateValueDisplay(); // Demo controls document.getElementById('disabled-checkbox').addEventListener('change', (e) => { interactiveCalendar.setDisabled(e.target.checked); }); document.getElementById('readonly-checkbox').addEventListener('change', (e) => { interactiveCalendar.setReadOnly(e.target.checked); }); document.getElementById('invalid-checkbox').addEventListener('change', (e) => { interactiveCalendar.setInvalid(e.target.checked); interactiveCalendar.setErrorMessage(e.target.checked ? 'Invalid date range selected' : ''); }); // Multi-month example const multiMonthCalendar = createRangeCalendar({ ariaLabel: 'Multi-month calendar', visibleDuration: { months: 3 }, onChange: (value) => console.log('Multi-month calendar changed:', value) }); document.getElementById('multi-month-example').appendChild(multiMonthCalendar); // Unavailable dates example const todayDate = today(); const unavailableRanges = [ { start: todayDate, end: addDays(todayDate, 5) }, { start: addDays(todayDate, 14), end: addDays(todayDate, 16) }, { start: addDays(todayDate, 23), end: addDays(todayDate, 24) } ]; function isDateInUnavailableRange(date) { const dateTime = new Date(date.year, date.month - 1, date.day).getTime(); return unavailableRanges.some(range => { const startTime = new Date(range.start.year, range.start.month - 1, range.start.day).getTime(); const endTime = new Date(range.end.year, range.end.month - 1, range.end.day).getTime(); return dateTime >= startTime && dateTime <= endTime; }); } const unavailableCalendar = createRangeCalendar({ ariaLabel: 'Calendar with unavailable dates', isDateUnavailable: isDateInUnavailableRange, onChange: (value) => console.log('Unavailable dates calendar changed:', value) }); document.getElementById('unavailable-dates-example').appendChild(unavailableCalendar); // Preset example const presetContainer = document.getElementById('preset-example'); const presetButtonsDiv = document.createElement('div'); presetButtonsDiv.className = 'preset-buttons'; let presetValue = null; const presetCalendar = createRangeCalendar({ ariaLabel: 'Calendar with presets', value: presetValue, onChange: (value) => { presetValue = value; console.log('Preset calendar changed:', value); } }); // Create preset buttons const presets = [ { label: 'This Week', start: addDays(todayDate, -todayDate.day), end: addDays(todayDate, 6 - todayDate.day) }, { label: 'Next Week', start: addDays(todayDate, 7 - todayDate.day), end: addDays(todayDate, 13 - todayDate.day) }, { label: 'This Month', start: createCalendarDate(todayDate.year, todayDate.month, 1), end: createCalendarDate(todayDate.year, todayDate.month, 30) } ]; presets.forEach(preset => { const button = document.createElement('button'); button.className = 'react-aria-Button'; button.textContent = preset.label; button.addEventListener('click', () => { presetValue = { start: preset.start, end: preset.end }; presetCalendar.setValue(presetValue); }); presetButtonsDiv.appendChild(button); }); presetContainer.appendChild(presetButtonsDiv); presetContainer.appendChild(presetCalendar); // International example const internationalCalendar = createRangeCalendar({ ariaLabel: 'International calendar', firstDayOfWeek: 'mon', onChange: (value) => console.log('International calendar changed:', value) }); document.getElementById('international-example').appendChild(internationalCalendar); ' width="100%" height="1200" frameborder="0" sandbox="allow-scripts allow-same-origin allow-popups">