DateRange Pilot
DateRange enables users to preview and select a range of days by picking dates from a calendar or adding a text input.
DateRange is distributed in its own package and must be installed separately.
also known as Calendar, Date time picker, Date picker range
Props
Usage guidelines
- When asking users to select past, present or future date ranges
- When users can pick between pre-selected date ranges or input a custom date range
- When users need to select one specific day. Use DatePicker instead
- When users need to input a date value with a numeric keyboard, for example when adding a birthday date. Use DateField instead
Best practices
- Disable future or past dates according to the use case. If the user would like to see predictions, for example, disable the past.
- When possible, provide a list of applicable date ranges to facilitate user selection
- Enable users to select dates in the future or past, if those dates are not a valid input
- Provide a long of a list of applicable date ranges with confusing labels, to avoid confusing the user. Display concise options and in a logical order.
Localization
Be sure to localize all text strings. Note that localization can lengthen text by 20 to 30 percent.
import { useState } from 'react'; import { DefaultLabelProvider, Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); return ( <DefaultLabelProvider labels={{ DateRange: { cancelText: 'Abbrechen', applyText: 'Anwenden', }, }} > <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateValue={{ startDate, endDate }} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onDateError={{ startDate: () => {}, endDate: () => {} }} onSubmit={() => {}} /> </Flex> </DefaultLabelProvider> ); }
Date format locales
DateRange supports multiple locales. Adjust the date format to each date-fns locale. The following locale examples show the different locale format variants.
Note that locale data from date-fns is external to gestalt-datepicker, it's not an internal dependency. Add date-fns to your app's dependencies.
import { DateRange } from 'gestalt-datepicker';
import { it } from 'date-fns/locale';
<DateRange localeData={it}/>
Use the SelectList to try out different locales by passing in the localeData
prop.
Variants
Controlled component
DateRange is a controlled component.
Use dateValue
, onDateChange
, onDateError
, onSubmit
and onCancel
to implement it correctly.
Follow the implementation in the example to implement a controlled DateRange correctly.
When there’s not a date range selected, the call-to-action is disabled to prevent user errors.
onSubmit
and onCancel
are optional props. When not used, make sure DateRange is accessible implementing Popover's onDismiss correctly, as shown in the example. Otherwise, DateRange might not be able to be dismissed if the user performs no changes in the data.
import { useRef, useState } from 'react'; import { Box, Flex, IconButton, Layer, Popover, Status } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [selectedDates, setSelectedDates] = useState(null); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [showComponent, setShowComponent] = useState(true); const anchorRef = useRef(null); return ( <Flex alignContent="start" height="100%" justifyContent="start" width="100%" > <Box padding={8}> <Flex alignItems="center" gap={2}> <Status subtext={ selectedDates ? `${ selectedDates[0].getMonth() + 1 }/${selectedDates[0].getDate()}/${selectedDates[0].getFullYear()} - ${ selectedDates[1].getMonth() + 1 }/${selectedDates[1].getDate()}/${selectedDates[1].getFullYear()}` : undefined } title={selectedDates ? 'Campaign dates selected' : 'Select dates'} type={selectedDates ? 'ok' : 'problem'} /> <IconButton ref={anchorRef} accessibilityLabel="Open calendar" icon="edit" onClick={() => { if (!showComponent && selectedDates) { setStartDate(selectedDates[0]); setEndDate(selectedDates[1]); } setShowComponent((value) => !value); }} selected={showComponent} /> </Flex> </Box> {showComponent ? ( <Layer> <Popover anchor={anchorRef.current} idealDirection="down" onDismiss={() => { setShowComponent(false); }} positionRelativeToAnchor={false} size="flexible" > <DateRange dateValue={{ startDate, endDate }} onCancel={() => { setStartDate(null); setEndDate(null); setShowComponent(false); }} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onSubmit={() => { if (startDate && endDate) setSelectedDates([startDate, endDate]); setShowComponent(false); }} /> </Popover> </Layer> ) : null} </Flex> ); }
With RadioGroup
This variant allow users to select common options of date ranges, so they can select an option without having to navigate through the calendar picker.
import { useMemo, useState } from 'react'; import { Flex, RadioGroup } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [period, setPeriod] = useState(null); const radioGroup = useMemo( () => ( <RadioGroup id="past radiogroup example" legend="Date range"> <RadioGroup.RadioButton checked={period === '1'} id="1" label="Next week" name="1" onChange={() => { setStartDate(new Date()); const now = new Date(); now.setDate(now.getDate() + 1 * 7); setEndDate(now); setPeriod('1'); }} value="1" /> <RadioGroup.RadioButton checked={period === '2'} id="2" label="Next 2 weeks" name="2" onChange={() => { setStartDate(new Date()); const now = new Date(); now.setDate(now.getDate() + 2 * 7); setEndDate(now); setPeriod('2'); }} value="2" /> <RadioGroup.RadioButton checked={period === '4'} id="4" label="Next 4 weeks" name="4" onChange={() => { setStartDate(new Date()); const now = new Date(); now.setDate(now.getDate() + 4 * 7); setEndDate(now); setPeriod('4'); }} value="4" /> <RadioGroup.RadioButton checked={period === 'custom'} id="custom" label="Custom" name="custom" onChange={() => setPeriod('custom')} value="custom" /> </RadioGroup> ), [period] ); return ( <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateValue={{ startDate, endDate }} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setPeriod('custom'); setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onSubmit={() => {}} radioGroup={radioGroup} /> </Flex> ); }
Use RadioGroup to select pre-established date ranges in the future. For example, activation dates for a new campaign.
import { useMemo, useState } from 'react'; import { Flex, RadioGroup } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [period, setPeriod] = useState(null); const radioGroup = useMemo( () => ( <RadioGroup id="past radiogroup example" legend="Date range"> <RadioGroup.RadioButton checked={period === '1'} id="1" label="Past week" name="1" onChange={() => { setEndDate(new Date()); const now = new Date(); now.setDate(now.getDate() - 1 * 7); setStartDate(now); setPeriod('1'); }} value="1" /> <RadioGroup.RadioButton checked={period === '2'} id="2" label="Past 2 weeks" name="2" onChange={() => { setEndDate(new Date()); const now = new Date(); now.setDate(now.getDate() - 2 * 7); setStartDate(now); setPeriod('2'); }} value="2" /> <RadioGroup.RadioButton checked={period === '4'} id="4" label="Past 4 weeks" name="4" onChange={() => { setEndDate(new Date()); const now = new Date(); now.setDate(now.getDate() - 4 * 7); setStartDate(now); setPeriod('4'); }} value="4" /> <RadioGroup.RadioButton checked={period === 'custom'} id="custom" label="Custom" name="custom" onChange={() => setPeriod('custom')} value="custom" /> </RadioGroup> ), [period] ); return ( <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateValue={{ startDate, endDate }} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setPeriod('custom'); setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onSubmit={() => {}} radioGroup={radioGroup} /> </Flex> ); }
Use RadioGroup to select pre-established date ranges in the past. For example, date ranges to analize performance metrics in ongoing campaigns.
Error messaging
DateRange can communicate errors when the user selects an invalid date. Use dateErrorMessage
, onDateError
, onDateBlur
, onDateFocus
, to implement error messaging correctly.
The following implementation shows how to use all required props for error messaging.
The onDateError
event are very noisy. If the date fields are not pre-populated, leverage onDateBlur
to validate the error state after the date fields lose focus. If the date fields are pre-populated leverage React's useEffect to validate the error state.
import { useEffect, useState } from 'react'; import { Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const date = new Date(); const year = date.getFullYear(); const minDate = new Date(year, 6, 1); const maxDate = new Date(year, 6, 31); const [startDate, setStartDate] = useState(new Date(year, 5, 1)); const [endDate, setEndDate] = useState(null); const [currentEndErrorMessage, setCurrentEndErrorMessage] = useState(null); const [currentStartErrorMessage, setCurrentStartErrorMessage] = useState(null); const [endErrorMessage, setEndErrorMessage] = useState(null); const [startErrorMessage, setStartErrorMessage] = useState(null); useEffect(() => { if (currentStartErrorMessage && currentStartErrorMessage[0]) { setStartErrorMessage('Select a valid date in July'); } }, [year, currentStartErrorMessage, startDate]); return ( <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateErrorMessage={{ startDate: startErrorMessage, endDate: endErrorMessage, }} dateValue={{ startDate, endDate }} maxDate={maxDate} minDate={minDate} onCancel={() => {}} onDateBlur={{ startDate: () => { if (currentStartErrorMessage && currentStartErrorMessage[0]) { setStartErrorMessage('Select a valid date in July'); } else { setStartErrorMessage(null); } }, endDate: () => { if (currentEndErrorMessage && currentEndErrorMessage[0]) { setEndErrorMessage('Select a valid date in July'); } else { setEndErrorMessage(null); } }, }} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onDateError={{ startDate: ({ errorMessage, value }) => { if (!errorMessage) { setStartErrorMessage(null); } setCurrentStartErrorMessage([errorMessage, value]); }, endDate: ({ errorMessage, value }) => { if (!errorMessage) { setEndErrorMessage(null); } setCurrentEndErrorMessage([errorMessage, value]); }, }} onSubmit={() => {}} /> </Flex> ); }
The secondary input fields also support error messages, you can control them with the secondaryDateErrorMessage
prop.
import { useEffect, useState } from 'react'; import { DeviceTypeProvider, Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const year = new Date().getFullYear(); const startDate = new Date(year, 5, 30); const endDate = null; const secondaryStartDate = new Date(year, 7, 20); const secondaryEndDate = new Date(year, 7, 21); const minDate = new Date(year, 6, 1); const maxDate = new Date(year, 6, 20); const startDateError = 'Select a date after June 1st'; const endDateError = 'Select a date before June 20th'; const [primaryErrorMessage, setPrimaryErrorMessage] = useState(null); const [secondaryErrorMessage, setSecondaryErrorMessage] = useState(null); const [startErrorMessage, setStartErrorMessage] = useState(null); const [endErrorMessage, setEndErrorMessage] = useState(null); useEffect(() => { if (primaryErrorMessage) { setStartErrorMessage(startDateError); } if (secondaryErrorMessage) { setEndErrorMessage(endDateError); } }, [primaryErrorMessage, secondaryErrorMessage]); return ( <DeviceTypeProvider deviceType="desktop"> <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateErrorMessage={{ startDate: startErrorMessage, endDate: null }} dateValue={{ startDate, endDate }} maxDate={maxDate} minDate={minDate} onCancel={() => {}} onDateChange={() => {}} onDateError={{ startDate: ({ errorMessage }) => { setPrimaryErrorMessage(errorMessage); }, endDate: () => {}, }} onSecondaryDateChange={() => {}} onSecondaryDateError={{ startDate: () => {}, endDate: ({ errorMessage }) => { setSecondaryErrorMessage(errorMessage); }, }} onSubmit={() => {}} secondaryDateErrorMessage={{ startDate: null, endDate: endErrorMessage, }} secondaryDateValue={{ startDate: secondaryStartDate, endDate: secondaryEndDate, }} /> </Flex> </DeviceTypeProvider> ); }
Disable past & future dates
DateField supports disabling future and past dates from being selected. Use minDate
for disabling past dates and maxDate
for disabling futures dates.
- Disable past. Disable the past when the user should select dates ranges in the future. For example, activation dates for a new campaign.
- Disable future. Disable the future when the user should select dates ranges in the past. For example, date ranges to analize performance metrics in ongoing campaigns.
import { useState } from 'react'; import { Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [startErrorMessage, setStartErrorMessage] = useState(null); return ( <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateErrorMessage={{ startDate: startErrorMessage, endDate: null }} dateValue={{ startDate, endDate }} minDate={new Date()} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onDateError={{ startDate: ({ errorMessage }) => setStartErrorMessage( errorMessage ? 'Please, enter a valid date' : null ), endDate: () => {}, }} onSubmit={() => {}} /> </Flex> ); }
import { useState } from 'react'; import { Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [endErrorMessage, setEndErrorMessage] = useState(null); return ( <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateErrorMessage={{ startDate: null, endDate: endErrorMessage }} dateValue={{ startDate, endDate }} maxDate={new Date()} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onDateError={{ startDate: () => {}, endDate: ({ errorMessage }) => setEndErrorMessage( errorMessage ? 'Please, enter a valid date' : null ), }} onSubmit={() => {}} /> </Flex> ); }
External handlers
DateRange consumes external handlers from GlobalEventsHandlerProvider.
Handlers:
- onRender: executed when DateField mounts for the first time
See GlobalEventsHandlerProvider for more information.
Mobile
DateRange requires DeviceTypeProvider to enable its mobile user interface. The example below shows the mobile platform UI and its implementation.
On mobile devices, the radiogroup
prop is not shown.
import { useState } from 'react'; import { DeviceTypeProvider, Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); return ( <DeviceTypeProvider deviceType="mobile"> <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateValue={{ startDate, endDate }} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onDateError={{ startDate: () => {}, endDate: () => {} }} onSubmit={() => {}} /> </Flex> </DeviceTypeProvider> ); }
Secondary date range
DateRange supports a secondary date range in case you need to handle more that one range, in order to enable it you just need to pass the props secondaryDateValue
and onSecondaryDateChange
. One scenario for this could be if you need to compare/monitor metrics of two periods of time when creating campaigns.
import { useState } from 'react'; import { DeviceTypeProvider, Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); const [secondaryStartDate, setSecondaryStartDate] = useState(null); const [secondaryEndDate, setSecondaryEndDate] = useState(null); return ( <DeviceTypeProvider deviceType="desktop"> <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateValue={{ startDate, endDate }} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onDateError={{ startDate: () => {}, endDate: () => {} }} onSecondaryDateChange={(newStartDate, newEndDate) => { setSecondaryStartDate(newStartDate.value); setSecondaryEndDate(newEndDate.value); }} onSubmit={() => {}} secondaryDateValue={{ startDate: secondaryStartDate, endDate: secondaryEndDate, }} /> </Flex> </DeviceTypeProvider> ); }
Read only
DateRange supports read-only date inputs, this option prevents the user from changing the date values from the date fields (not from interacting with the fields).
import { useState } from 'react'; import { Flex } from 'gestalt'; import { DateRange } from 'gestalt-datepicker'; export default function Example() { const [startDate, setStartDate] = useState(new Date()); const [endDate, setEndDate] = useState(new Date()); return ( <Flex alignItems="center" height="100%" justifyContent="center" width="100%" > <DateRange dateValue={{ startDate, endDate }} onCancel={() => {}} onDateChange={(newStartDate, newEndDate) => { setStartDate(newStartDate.value); setEndDate(newEndDate.value); }} onDateError={{ startDate: () => {}, endDate: () => {} }} onSubmit={() => {}} readOnly /> </Flex> ); }
Writing
- Use concise labels to indicate what the date range selection is referring to
- Add long and complicated labels to the date range picker and to the RadioGroup labels
Component quality checklist
Quality item | Status | Status description |
---|---|---|
Figma Library | Component is not currently available in Figma. | |
Responsive Web | Component does not respond to changing viewport sizes in web and mobile web. |
Internal documentation
Related
DateField
DateField is used when the user has to select a date. Compared to DatePicker, DateField has no supporting calendar to select a date, the user must input date values with a numeric keyboard.
DatePicker
DatePicker is used when the user has to select a date. Compared to DateField, DatePicker has a supporting calendar to select a date.