An input component that allows selecting a date from a popup calendar.

Importing

The component can be imported via:

import { DatePicker } from '@customerio/pluma-components/react';

Date values

The PlumaDatePicker component uses the @internationalized/date package for handling date values. This means it expects CalendarDate instances in the value prop, and returns them in the onChange callback as well.

A CalendarDate can be created using its constructor:

import { CalendarDate } from '@internationalized/date';

// January 30, 2012
const date = new CalendarDate(2012, 1, 30);

Or by parsing an ISO 8601 formatted string with the parseDate function:

import { parseDate } from '@internationalized/date';

const date = parseDate('2012-01-30');

When withTime is enabled, the component will return ZonedDateTime instances that include time and timezone information.

See the CalendarDate documentation or the ZonedDateTime documentation for more details.

A native Date object can be converted to a CalendarDate like this:

const nativeDate = new Date();

const date = new CalendarDate(
	nativeDate.getFullYear(), 
	nativeDate.getMonth() + 1, 
	nativeDate.getDate()
);

A CalendarDate can be converted back to a native Date object with the toDate method, for example:

import { getLocalTimeZone } from '@internationalized/date';

const date = new CalendarDate(2012, 1, 30);
const nativeDate = date.toDate(getLocalTimeZone());

For more information, see the CalendarDate conversion docs.

Usage

To use the date picker, at a minimum you must provide the following props:

  • value
    • a CalendarDate or ZonedDateTime instance
  • onChange
    • a function, which receives a CalendarDate or ZonedDateTime instance for the date selected in the calendar
  • label

For example:

import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'

export default function Example() {
	const [value, setValue] = useState(null);

	return <DatePicker
		label="Select a date"
		value={value}
		onChange={(value) => {
			setValue(value);
		}}
	/>
}

Date ranges

The date picker supports selecting date ranges, which can be enabled by setting the isRange prop to true.

When enabled, the value prop should be an object with start and end properties, each containing a CalendarDate instance.

Similarly, the value in the onChange callback will also be an object of that shape.

Date constraints

You can constrain the selectable dates in two ways:

Using minDate and maxDate

The simplest approach is to use the minDate and maxDate props to define a valid date range. Dates outside this range will be disabled and visually indicated. Users will not be able to select dates outside the specified range.

You can use either one or both constraints:

  • Use only minDate to enforce a minimum selectable date with no upper limit
  • Use only maxDate to enforce a maximum selectable date with no lower limit
  • Use both to restrict selection to a specific date range

Both minDate and maxDate accept a CalendarDate instance:

import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'
import { CalendarDate, parseDate } from '@internationalized/date'

export default function Example() {
  const [value, setValue] = useState(null);
  // Set minimum date to 2 days ago
  const today = new Date();
  const twoDaysAgo = new Date(today);
  twoDaysAgo.setDate(today.getDate() - 2);
  
  // Set maximum date to 5 days in the future
  const fiveDaysFromNow = new Date(today);
  fiveDaysFromNow.setDate(today.getDate() + 5);
  
  // Convert to CalendarDate objects
  const minDate = new CalendarDate(
    twoDaysAgo.getFullYear(),
    twoDaysAgo.getMonth() + 1,
    twoDaysAgo.getDate()
  );
  
  const maxDate = new CalendarDate(
    fiveDaysFromNow.getFullYear(),
    fiveDaysFromNow.getMonth() + 1,
    fiveDaysFromNow.getDate()
  );

  return <DatePicker
    label="Select a date (limited range)"
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
    minDate={minDate}
    maxDate={maxDate}
  />
}

You can also use just one constraint if needed:

import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'
import { CalendarDate } from '@internationalized/date'

export default function Example() {
  const [value, setValue] = useState(null);
  
  // Only set a minimum date (today)
  const today = new Date();
  const minDate = new CalendarDate(
    today.getFullYear(),
    today.getMonth() + 1,
    today.getDate()
  );

  return <DatePicker
    label="Select a future date"
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
    minDate={minDate}
  />
}

These constraints also work with date ranges:

import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'
import { CalendarDate } from '@internationalized/date'

export default function Example() {
  const [value, setValue] = useState(null);
  
  // Set constraints for one month range
  const today = new Date();
  const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
  const lastDayOfMonth = new Date(today.getFullYear(), today.getMonth() + 1, 0);
  
  const minDate = new CalendarDate(
    firstDayOfMonth.getFullYear(),
    firstDayOfMonth.getMonth() + 1,
    firstDayOfMonth.getDate()
  );
  
  const maxDate = new CalendarDate(
    lastDayOfMonth.getFullYear(),
    lastDayOfMonth.getMonth() + 1,
    lastDayOfMonth.getDate()
  );

  return <DatePicker
    label="Select dates within this month"
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
    isRange={true}
    minDate={minDate}
    maxDate={maxDate}
  />
}

Using the isDateDisabled function

For more complex date constraints, you can use the isDateDisabled function prop. This gives you complete control over which dates are selectable.

The isDateDisabled function takes a CalendarDate parameter and should return true if the date should be disabled, or false if it should be enabled. It will take precedence over the minDate and maxDate props.

import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'
import { CalendarDate } from '@internationalized/date'

export default function Example() {
  const [value, setValue] = useState(null);
  
  // Disable weekends (Saturday and Sunday)
  const isDateDisabled = (date) => {
    // Get JavaScript day of week (0 = Sunday, 6 = Saturday)
    const day = date.toDate('America/Los_Angeles').getDay();
    return day === 0 || day === 6;
  };

  return <DatePicker
    label="Select a weekday"
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
    isDateDisabled={isDateDisabled}
  />
}

Important notes about date constraints:

  1. When using date constraints with range selection (isRange={true}), both the start and end dates must fall within the allowed range.
  2. The date constraints are enforced for both manual selection and when using presets.
  3. The constraints are visually indicated with a strikethrough style, so users can easily see which dates are unavailable.
  4. If you programmatically set a value that contains dates outside the allowed range, they will still be displayed, but the user will be unable to select new dates outside the range.
  5. Both minDate and maxDate are inclusive - users can select the exact date specified as the min/max boundary.
import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'

export default function Example() {
	const [value, setValue] = useState(null);

	return <DatePicker
		label="Select a date"
		value={value}
		onChange={(value) => {
			setValue(value)
		}}
		isRange={true}
	/>
}

Date range presets

To make it easier to select common date ranges, the date picker supports a withPresets prop, which will render a list of presets next to the calendar.

This is only available when isRange is true.

import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'

export default function Example() {
	const [value, setValue] = useState(null);

	return <DatePicker
		label="Select a date"
		value={value}
		onChange={(value) => {
			setValue(value)
		}}
		isRange={true}
		withPresets={true}
	/>
}

Custom presets

The list of presets shown can be customized, by passing an array of preset objects into the presets prop.

A preset object can be one of two types: relative or custom.

A relative preset requires:

  • type: 'relative'
    • this key tells the date picker component to treat the preset as the "relative" type
  • label
    • a string to show in the preset button
  • timeAgo
    • an object to configure the relative date range to select when this button is clicked

timeAgo should contain two properties:

  • unit
    • a string (a key from the @internationalized/date's DateTimeDuration type), one of 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'.
  • value
    • a number, the amount of unit to go back from today

A custom preset allows for more flexibility in creating the date range. It requires:

  • type: 'custom'
  • label
  • callback
    • a function that returns an object with start and end properties, each containing a CalendarDate instance. This range will be set when the preset is clicked.
    • the callback is called with an object containing the keys:
      • localTimeZone - a string representing the current time zone (either the local time zone, or the one set via the DatePicker's timeZone prop)
import { 
	startOfMonth, 
	endOfMonth, 
	getLocalTimeZone, 
	today,
} from '@internationalized/date';
import { DatePicker } from '@customerio/pluma-components/react'
import { useState } from 'react'

export default function Example() {
	const [value, setValue] = useState(null);

	return <DatePicker
		label="Select a date"
		value={value}
		onChange={(value) => {
			setValue(value)
		}}
		isRange={true}
		withPresets={true}
		presets={[
			{
				type: 'relative',
				label: 'Last 3 days',
				timeAgo: {
					unit: 'days',
					value: 2
				}
			},
			{
				type: 'custom',
				label: 'Current month',
				callback: () => {
					const _today = today(getLocalTimeZone());
					const start = startOfMonth(_today);
					const end = endOfMonth(_today);
					return { start, end };
				}
			},
		]}
	/>
}

The default presets and preset types can also be imported from Pluma via:

import {
	PLUMA_DATE_PICKER_PRESET_LAST_24_HOURS,
	PLUMA_DATE_PICKER_PRESET_LAST_7_DAYS,
	PLUMA_DATE_PICKER_PRESET_LAST_30_DAYS,
	PLUMA_DATE_PICKER_PRESET_LAST_365_DAYS,
	// Selects the last 12 weeks, ending on today (included)
	PLUMA_DATE_PICKER_PRESET_LAST_12_WEEKS_FROM_NOW,
	// Selects the last 12 weeks, where each week starts on a Sunday
	PLUMA_DATE_PICKER_PRESET_LAST_12_FULL_WEEKS_SUNDAY_START,
	// An array containing the default presets used by the datepicker
	PLUMA_DATE_PICKER_DEFAULT_PRESETS,
} from '@customerio/pluma-components/react/date-picker'
import type {
	PlumaDatePickerPresetRelative,
	PlumaDatePickerPresetCustom,
	PlumaDatePickerPreset,
} from '@customerio/pluma-components/react/date-picker'

Enabling/disabling presets

By default, presets will be automatically disabled if their resulting date range falls (even partially) outside the date constraints set by minDate and maxDate. However, sometimes it's desirable to have the presets enabled anyway, allowing the selection of partial ranges.

The isPresetDisabled boolean can control this behavior. It can receive either:

  • a boolean value, which will apply to all presets
  • a function, which receives a preset object and its current calculated range, and should return a boolean indicating whether the preset should be disabled
import { DatePicker, InlineStack } from '@customerio/pluma-components/react'
import { useState } from 'react'
import { today, getLocalTimeZone } from '@internationalized/date';

const minDate = today(getLocalTimeZone()).subtract({ days: 3 });

export default function Example() {
	const [value, setValue] = useState(null);

	return <InlineStack gap="200" alignBlock="center">
		<DatePicker
			label="Select a date"
			value={value}
			onChange={(value) => {
				setValue(value)
			}}
			isRange={true}
			withPresets={true}
			minDate={minDate}
		/>
		<DatePicker
			label="Select a date"
			value={value}
			onChange={(value) => {
				setValue(value)
			}}
			isRange={true}
			withPresets={true}
			minDate={minDate}
			isPresetDisabled={false}
		/>
	</InlineStack>;
}

Range length limits

By default, the date picker allows selecting a range of any length - including a single day. To enforce a minimum and/or maximum range length, the minRange and maxRange props can be used.

Both props accept multiple formats:

import { DatePicker } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <DatePicker
		label="Select a date"
		value={value}
		onChange={(value) => {
			setValue(value)
		}}
		isRange={true}
		minRange={3}
		maxRange="3 weeks"
	/>
}

Time zones

By default, PlumaDatePicker will use the user's local time zone when working with dates: most importantly, determining what "today" is for pre-selecting today's date, calculating relative ranges etc.

However, sometimes it's necessary to show a DatePicker that refers to dates in a different time zone. To make sure the component handles disabled dates, presets, etc correctly, a localTimeZone string prop can be provided.

The time zone string is passed into Intl API calls, and should be an IANA time zone name. If an invalid time zone is provided, it will fall back to @internationalized/date's getLocalTimeZone function.

import { DatePicker } from '@customerio/pluma-components/react';
import { useState } from 'react';
import { today } from '@internationalized/date';

export default function Example() {
	const [value, setValue] = useState(null);

	return <DatePicker
		label="Select a date"
		value={value}
		onChange={(value) => {
			setValue(value)
		}}
		isRange={true}
		withPresets={true}
    localTimeZone="Australia/Melbourne"
    maxDate={today('Australia/Melbourne')}
	/>
}

Customizing labels

If a string label is not sufficient, a component can be used to render the label.

In React, the label prop accepts any ReactNode.

import { DatePicker, Icon } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
  const [value, setValue] = useState(null);

  return <DatePicker
    label={<>Custom label <Icon name="campaigns" isInline={true} size="sm" /></>}
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
  />
}

Customizing descriptions

If a string description is not sufficient, a component can be used to render the description.

In React, the description prop accepts any ReactNode.

import { DatePicker, Link } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
  const [value, setValue] = useState(null);

  return <DatePicker
    ariaLabel="Custom description input"
    description={<>Custom description, <Link href="https://docs.customer.io" isExternal={true} variant="secondary">see docs for details</Link></>}
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
  />
}

Customizing errors

If a string error is not sufficient, a component can be used to render the error.

In React, the error prop accepts any ReactNode.

import { DatePicker, Link } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
  const [value, setValue] = useState(null);

  return <DatePicker
    ariaLabel="Custom error input"
    error={<>Custom error, <Link href="https://docs.customer.io" isExternal={true} variant="secondary">see docs for help</Link></>}
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
  />
}

API

Element (or elements) that describe the form field. Also used for error messages, as aria-errormessage isn't supported in all assistive technologies: https://a11ysupport.io/tech/aria/aria-errormessage_attribute

String value that labels the form field.

Element (or elements) that label the form field.

Focuses automatically when the component is rendered. Should be used with care and only sparingly, as there are some accessibility concerns. More information here

Additional classes to be applied to the top-level container. This is necessary as an argument in Ember, where we apply splattributes on the "input" itself, which means we can't pass a custom class to the containing element the classic way.

Whether the date picker should render in the open state.

A description for the field, providing additional context or hints.

An error message associated with the form field. Boolean: Ember-only. If the error named block is used, this prop can be set to true to indicate an error is present, which will make the error block render.

An optional id to be assigned to the input element. Also used to generate ids for labels and error messages. If one isn't provided, it will be generated.

Whether the input should render in an "active" styled state.

Whether the input element is clearable.

Function to determine if a date is disabled and cannot be selected. This takes precedence over minDate and maxDate if provided.

Whether the form field is disabled.

Whether the form field is in an invalid state.

Whether the input should show a loading spinner.

By default, a preset will become disabled if its range cannot be fully selected (based on dates being disabled via minDate and maxDate). This allows overriding that behavior to enable/disable presets conditionally, or keep them always enabled. If a function is provided, it will be called with the preset and its calculated range, and should return a boolean. If the value is a boolean, it will apply to all presets.

Whether the date picker should select date ranges.

The label text to show with the form field.

By default, the DatePicker uses the user's local time zone for calculating dates like "today". If the date picker represents dates in a different time zone, a time zone name can be provided here.

The maximum selectable date.

The maximum range length that must be selected. By default, there is no maximum range length. It accepts multiple formats:

The minimum selectable date.

The minimum length of a time range that must be selected. By default, there is no minimum range length (essentially 0 - same day selection allowed). It accepts multiple formats:

The name attribute is used for HTML forms

In addition to isClearable, this function is required to render the clear button. This will be called when the clear button is clicked.

Placeholder text to show inside the field.

The duration (in ms) for the popover animation.

An array of presets to render instead of the default ones.

Whether to allow browser autofill and password managers. When false (default), adds attributes to disable password managers (1Password, LastPass, Bitwarden, Dashlane). Set to true for fields where autofill is desired (e.g., email, password, address fields).

Whether the generated description id should be included in the input's aria-describedby attribute. Disable this when a label`` tag surrounds the input as well as it's description text, for example in an OptionCard` component.

Whether the generated label id should be included in the input's aria-labelledby attribute. Disable this when the label tag surrounds the input as well as it's label text, for example in an OptionCard component.

Default:true

Whether the calendar should automatically focus when it first renders.

Whether or not the password visibility button will be shown. Defaults to true.

By default, form elements will throw an error if no label or aria-label is provided. Disable this to suppress the error, for example when building a custom form element, and you want to handle labeling yourself.

Default:md

The size of the input element. medium and small are deprecated, use md and sm instead.

The type of input that should be rendered. Defaults to text.

Pluma internal property to set a component name used for logging. Only use if building a custom component that wraps this one and need to make it more obvious where errors, for example, come from.

Additional classes to be applied to the description element.

Additional classes to be applied to the error element.

Additional classes to be applied to the field node.

Additional classes to be applied to the footer element.

Ember-only: this is used internally to detect whether a label named block is present, and to ignore empty label props if so.

Additional classes to be applied to the input node.

Sets the size attribute on the input element. This controls the visible width of the input in characters.

Additional classes to be applied to the div that wraps the input element.

Whether the component should render with "legacy" styles.

Additional classes to be applied to the label node.

Additional inline styles to apply on the popover component (the floating element, not the trigger). Should only be used as a last resort

Whether to apply the center-baseline variant.

Whether the date picker should show preset selection. Only available when isRange is true.

Default:false

Whether to include time selection in the picker.

Ember only: a modifier to apply on the input's wrapper element.

React only: a ref to the input's wrapper element.

The text to show for the preset button.

The time period (from now into the past) to select. unit is an @internationalized/date DateTimeDuration key, one of: 'years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds', 'milliseconds'. value is how many of that unit to go back.

The type of preset.

The text to show for the preset button.

The type of preset.