On this page
Datepicker Field
A form field for entering a date, with an optional calendar popup for selection
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- When the user needs to enter a date that is in the recent past or near future, where seeing a calendar helps them pick the right day
- When the user might copy the date from another source (such as a letter or document) and type it directly
- When the date relates to a booking, appointment, or event where surrounding context (day of the week, proximity to today) matters
When not to use
- For historical dates the user knows from memory, such as date of birth or passport issue date. The calendar offers no benefit and navigating back years is tedious. Use DateField instead.
- For dates far in the future where the user would need to navigate many months forward. Consider a Text Field with clear format instructions if the date range is very large.
- To indicate that a field is unavailable - the disabled state is not supported. Use Summary List to show values that cannot be edited, or conditionally hide the field if it is not relevant in the current context.
Anatomy

- Label
- Required/Optional indicator
- Hint (optional; describe constraints such as the available date range rather than the input format)
- Input (accepts a date typed in dd/mm/yyyy format)
- Calendar button (opens the calendar popup)
- Error message
- Calendar popup (contains month navigation, a day grid, a today button, and a clear button)
- Today indicator (highlights today's date in the calendar grid)
Basic usage
Edit live code
<DatepickerField id="date" label="Date" />
Variants and features
OverlayProvider wrapper
DatepickerField requires an OverlayProvider wrapper to position the calendar popup correctly and make it accessible. Wrap the page or section that contains the datepicker, not each individual field.
Without it the popup is not accessible: the page behind the calendar is not hidden from screen readers, so assistive technology users can read content they cannot interact with.
This example is not live, because the docs site provides its own OverlayProvider around every page.
import { OverlayProvider } from '@react-aria/overlays';
<OverlayProvider>
{/* One per page or section, not one per field */}
<DatepickerField id="start-date" label="Start date" />
<DatepickerField id="end-date" label="End date" />
</OverlayProvider>
Typing a date directly
Users can type a date directly into the input without opening the calendar. The expected format is dd/mm/yyyy. The placeholder shows this format automatically.
The calendar updates to reflect the typed value as soon as it parses as a valid date.
Unavailable dates
Use the isDateUnavailable prop to mark specific dates as unavailable in the calendar. Unavailable dates are still visible but cannot be selected.
This does not stop a date being typed into the input, so always validate on submit as well.
Edit live code
<DatepickerField id="appointment-date" label="Appointment date" hint="Weekdays only" isDateUnavailable={date => date.getDay() === 0 || date.getDay() === 6} />
Validating the selected date
Use the exported getDatepickerDate utility to parse the field's string value into a Date object. It returns null if the value is not a valid date.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [errors, setErrors] = useState([]); const onSubmit = (e) => { e.preventDefault(); const newErrors = []; const { datepicker } = Object.fromEntries(new FormData(e.currentTarget)); const datepickerDate = getDatepickerDate(datepicker) if (!datepickerDate) { newErrors.push({ id: 'datepicker', message: 'Enter a valid date in the format dd/mm/yyyy'}); } setErrors(newErrors); if (!newErrors.length) { alert('success'); } } return ( <Form errors={errors} onSubmit={onSubmit} noValidate> <FormFields> <DatepickerField id="datepicker" name="datepicker" label="Datepicker example" /> </FormFields> <FormActions> <PrimaryCTAButton type="submit">Validate date</PrimaryCTAButton> </FormActions> </Form> ); } return <Example />; }
Date ranges
DatepickerField does not have a built-in date range mode. To collect a start and end date, place two DatepickerField components side by side. Use isDateUnavailable on the second field to prevent the user from selecting a date before the first.
Align the two fields to the bottom of their container so the inputs stay aligned when one has hint text or an error.
Edit live code
() => { // ignore - for live code example only. const Example = () => { const [startDate, setStartDate] = useState(''); const isDateUnavailable = (date) => { const start = getDatepickerDate(startDate); if (!start) { return false; } return date.getTime() < start.getTime(); }; return ( <div style={{ display: 'flex', gap: '1.2rem', alignItems: 'flex-end' }}> <DatepickerField id="range-start-date" name="range-start-date" label="Start date" hint="On or after this date" onChange={setStartDate} /> <DatepickerField id="range-end-date" name="range-end-date" label="End date" isDateUnavailable={isDateUnavailable} /> </div> ); } return <Example />; }
Common form features
The following features are shared across all Nebula form fields. See Form guidance for full detail.
Validation and errors: when a field is invalid, an error message appears below the input. Use getDatepickerDate to check whether the string value is a valid date before deciding whether to show an error.
Hint text: use hint text to describe date constraints (for example, "Within the next 28 days"), not the input format. The placeholder already shows dd/mm/yyyy.
Required and optional: fields are required by default. Use the optional prop to mark a field as optional.
Field width: DatepickerField defaults to a fixed width that fits the dd/mm/yyyy format. Use fullWidth to control responsive behaviour.
usage
- DatepickerField must not be used to collect a date range from a single fieldDatepickerField collects a single date. Use two side-by-side DatepickerField components for a start and end date.Copy rule ID for datepickerfield.date-range-two-fields
- DatepickerField must not be passed the disabled propThe prop passes through to the native input with no Nebula styling and removes the field from the tab order, making it invisible to keyboard users with no accessible indication of why.Copy rule ID for datepickerfield.no-disabled
- DatepickerField should be replaced with plain text. For example in a SummaryList. When displaying a value the user cannot editA field that looks interactive but cannot be changed is confusing. Plain text communicates the value without implying editability.Copy rule ID for datepickerfield.non-editable-as-text
- DatepickerField should not be used for historical dates the user is expected to know from memory, such as date of birthNavigating back years in a calendar is tedious. Use DateField for dates the user knows by heart that predate calendar-based selection.Copy rule ID for datepickerfield.not-for-historical-known-dates
content
- DatepickerField hint text should describe date constraints rather than the input formatThe placeholder already shows the dd/mm/yyyy format. Hint text adds more value when it tells the user what dates are valid, for example "Within the next 28 days".Copy rule ID for datepickerfield.hint-describes-constraints
- DatepickerField hint text should not repeat the date format in hint textThe input already shows a dd/mm/yyyy placeholder. Restating the format adds noise without helping the user.Copy rule ID for datepickerfield.no-format-in-hint
interaction
- DatepickerField calendar must not hide unavailable dates from the calendar gridUnavailable dates must remain visible so users understand the full shape of the calendar. Use isDateUnavailable to mark them as unselectable, not to hide them.Copy rule ID for datepickerfield.unavailable-dates-not-removed
- DatepickerField calendar may have the today button used by the user to navigate quickly to the current dateStated here as an explicit permission. The today button is always present and should not be removed.Copy rule ID for datepickerfield.today-button
validation
- DatepickerField should use getDatepickerDate to parse and validate the field valuegetDatepickerDate handles the dd/mm/yyyy parsing and returns null for invalid dates, including partial inputs. Avoid reimplementing the parsing logic.Copy rule ID for datepickerfield.validate-with-utility
composition
- DatepickerField must be rendered inside an OverlayProviderThe calendar popup uses React Aria's overlay system. Without OverlayProvider the popup will not be accessible and may render in the wrong position.Copy rule ID for datepickerfield.overlay-provider
- OverlayProvider should not be placed around each individual DatepickerFieldOne OverlayProvider per page or section is sufficient and avoids stacking overlay contexts unnecessarily.Copy rule ID for datepickerfield.overlay-provider-not-per-field
DatepickerField
Also accepts all props from HTMLAttributes, InputHTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
id | string | — | Yes | Unique id for the input, used to link the label, hint and error to it. Also used as the base for related ids such as `{id}-error`. |
error | ReactNode | — | No | Error message shown below the input. When set, the input gets `aria-invalid` and an error border, and the message is linked to it with `aria-describedby`. Takes precedence over an error provided by a surrounding `Form`. |
label | string | — | Yes | Visible label for the field. It is associated with the input, so it forms the input's accessible name. |
name | string | — | No | The `name` attribute passed to the input. Also used to look up the field's error from a surrounding `Form`. |
hint | ReactNode | — | No | Helper text shown below the label. It is part of the label element, so it is read as part of the input's accessible name. |
optional | boolean | false | No | Marks the field as not required. This appends "(optional)" to the label (or, when the theme marks required fields, hides the required asterisk) and sets `aria-required` on the input only when the field is not optional. |
fullWidth | always never small | 'small' | No | Controls when the field stretches to the full width of its container. `always` is always full width, `never` always uses the width set by `characters`, and `small` is full width on small screens only. |
characters | number | 10 | No | Width of the input, as an approximate number of characters (the width is set to `0.8em` per character, rounded up). Does not limit how much can be typed. |
affixWidth | number | 40 | No | Extra width in pixels added to the width calculated from `characters`, to make room for icons or buttons inside the input. |
iconLeft | IconName | (string & {}) | — | No | Name of an icon to show inside the left of the input. The input padding increases to make room for it. |
iconRight | IconName | (string & {}) | — | No | Name of an icon to show inside the right of the input. The input padding increases to make room for it. |
onChange | (value: string) => void | — | No | Called with the new text when the user types (exactly as typed, not validated or reformatted) or picks a date from the calendar (formatted `dd/mm/yyyy`), and with an empty string when the date is cleared from the calendar. |
defaultValue | string | — | No | Initial text of the input when uncontrolled, in `dd/mm/yyyy` format. |
defaultOpen | boolean | — | No | Whether the calendar popover is open when first rendered. |
isDateUnavailable | IsDateUnavailable | — | No | Called for each date in the calendar. Return `true` to make that date unavailable so it cannot be selected in the calendar. It does not stop a date being typed into the input. |
This component has no styles of its own to document.
In brief
Datepicker Field is a text input for typing a date as dd/mm/yyyy, with a calendar button that opens a calendar in a dialog. A screen reader announces the label, then a text field, and the button is a separate stop that opens the calendar.
What Nebula provides
- A real label linked to the text input, aria-required unless the field is optional, and an error linked with aria-invalid and aria-describedby.
- A calendar button with aria-haspopup="dialog" and aria-expanded, built on React Aria.
- A calendar dialog that keeps focus inside while it is open, returns focus when it closes, closes with Esc or a click outside, and has a hidden dismiss button for screen reader users.
- A calendar grid that announces the visible month when it changes. Dates marked unavailable are dimmed and cannot be selected in the calendar.
Keyboard behaviour
The input and then the calendar button are tab stops. In the calendar, the arrow keys move between days, Page Up and Page Down change month, Home and End move within the week, Enter or Space selects a day and Esc closes it.
Focus order
- The date input
- The calendar button
- Inside the open calendar: the year and month buttons, the day grid, then Today and Clear
Interactions
- Tab - Moves focus to the input, then to the calendar button
- Enter or Space - Opens the calendar from the calendar button
Accessible design
Always put the date format in the hint, not only in the placeholder
The placeholder (dd/mm/yyyy) disappears as soon as the user types and is shown at half opacity, so a user who is partway through a date loses the format. Write it in the hint, for example "For example, 21/02/2024".
WCAG: 3.3.2 Labels or Instructions
If some dates are unavailable, say which in the hint
The calendar only dims them, and a user who types a date is not stopped. A short hint such as "Monday to Friday" tells every user the rule before they choose.
WCAG: 3.3.2 Labels or Instructions
Accessible development
Always validate the typed text on submit, and say what is wrong
The input accepts any text and isDateUnavailable does not apply to it. Check it with getDatepickerDate and show an error that names the format or the rule it broke.
WCAG: 3.3.1 Error Identification 3.3.3 Error Suggestion
Always pass errors through error, or through a Form with the field name
This is what sets aria-invalid and links the message to the input. Text placed beside the field is not announced with it.
WCAG: 3.3.1 Error Identification
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Partially tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Partially tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.