Skip to content

Date Picker

A field that opens a Calendar. It is that composition and two things the composition always needs anyway: one place that owns the selection, because the field's text and the grid sit on opposite sides of a portal, and a rule for when picking a day closes the popup, which the calendar cannot decide because it does not know it is in one.

date-picker/basic.tsx

Import

import { DatePicker } from "@forte-ui/react";

Eight parts. The full anatomy, with every optional layer present:

<DatePicker.Root mode="single" selected={date} onSelect={setDate}>
  <DatePicker.Trigger aria-label="Due date">
    <DatePicker.Value placeholder="Pick a date" />
    <DatePicker.Icon />
  </DatePicker.Trigger>
  <DatePicker.Popup>
    <DatePicker.Calendar />
    <DatePicker.Footer>
      <DatePicker.Clear />
    </DatePicker.Footer>
  </DatePicker.Popup>
</DatePicker.Root>

Most pickers need less: the demo above is Root → Trigger → Popup and nothing else.

Selection

Root owns it, and takes the same three modes Calendar does — single, multiple, range — with selected typed to match. Pass selected with onSelect to control it; leave it off and the picker keeps its own state.

Controlled-ness is decided from whether selected was passed, not from whether it is currently defined, so the idiomatic useState<Date>() starting at undefined does not silently begin life uncontrolled.

When the popup closes

closeOnSelect follows the mode unless you say otherwise:

modeCloses when
singlea day is picked — but not when one is cleared, where a vanishing popup leaves you unsure it took
rangeboth ends are in, the only moment the answer is complete
multiplenever — you are still choosing, and closing after the first day would cost two clicks for the second
date-picker/range.tsx

Picking several days

date-picker/footer.tsx

A multiple picker never closes on its own, so it needs a way out that is not the Escape key. DatePicker.Footer is the actions row for that, and because the surface really is a popover, a plain Popover.Close works inside it.

DatePicker.Clear empties the selection. It disables itself when there is nothing to clear and when Root is required — disabled rather than hidden, because a control that disappears as you reach for it is worse than one that is visibly unavailable, and the popup would reflow under the pointer.

Bounds and disabled days

date-picker/constrained.tsx

DatePicker.Calendar forwards every Calendar prop it does not own itself, so minDate, maxDate, disabled, numberOfMonths, captionLayout, showWeekNumbers and the rest behave exactly as they do on a bare calendar. What it does own — and therefore does not forward — is mode, selected, onSelect, required and locale, all of which come from Root so the field and the grid cannot disagree.

In a form

date-picker/with-field.tsx
Starts on

Invitations go out that morning.

The trigger is a <button>, so pair it with <Field.Label nativeLabel={false}>. A native <label> would hand the trigger its :hover state and open the calendar on every label click; with it off the two are wired together with aria-labelledby instead — which is also what gives the trigger its accessible name, so no aria-label is needed here.

That wiring is not automatic for a popover trigger the way it is for an Input or a Select.Trigger, which read the field context themselves. DatePicker.Trigger renders through Base UI's Field.Control to get it — so the label and the Field.Description both reach the button, and outside a Field.Root nothing changes.

The picker holds no form value of its own. Submit the date from your own state, or render a hidden input beside the trigger:

<input type="hidden" name="starts" value={date ? date.toISOString().slice(0, 10) : ""} />

Sizes and variants

date-picker/sizes.tsx

Same two axes as every other control in the library: size for the field's height, padding and text; variant for how loud it is. outline reads as a form control, soft as a filled field, ghost as an inline affordance.

Formatting the field

The trigger shows a medium date in Root's locale. range renders as from – to with an en dash, and multiple collapses past two days into Aug 27, 2026 +3 — a field that grows as you pick would reflow the form around it, and the full list is in the popup either way.

formatValue replaces all of that:

<DatePicker.Root
  selected={date}
  onSelect={setDate}
  formatValue={(value) =>
    value ? new Intl.DateTimeFormat("en-GB", { dateStyle: "full" }).format(value) : null
  }
>

Return null to fall back to DatePicker.Value's placeholder.

Accessibility

The trigger must have an accessible name — a <Field.Label nativeLabel={false}> in the same <Field.Root>, or an aria-label. In development it warns once if it ends up with neither.

Keyboard interactions
KeyBehaviour
Enter then SpaceOpens the calendar from the field.
EscapeCloses the calendar and returns focus to the field.
TabInside the popup, moves between the grid and the footer's buttons. The grid itself is one stop — the arrows move within it.

Everything else is the calendar's, which is documented on its own page: a real <table role="grid">, a roving tabindex, and days that are aria-disabled rather than disabled so navigation can cross them.

DatePicker.Calendar turns autoFocus on by default, the opposite of Calendar's own default. There is a press behind this calendar, so the first arrow key should move a day rather than do nothing; a calendar sitting in the page has no such press, which is why the base component leaves focus where it was.

The glyph is aria-hidden and takes no pointer events, so the field's own cursor shows through it. The value is the trigger's text, which means a screen reader announces the label and the chosen date together, with no extra wiring.

Theming

Every property below is declared on DatePicker.Trigger, so override them there — through its className or an inline style — not on an ancestor, where the part's own declaration would beat the inherited value.

Theming tokens for DatePicker
PropertyControlsDefault
--forte-date-picker-trigger-heightField height (reset per size)var(--forte-control-h-md)
--forte-date-picker-trigger-padding-xField inline padding (reset per size)var(--forte-control-px-md)
--forte-date-picker-trigger-font-sizeField font size (reset per size)var(--forte-font-size-2)
--forte-date-picker-trigger-gapGap between the value and the glyphvar(--forte-control-gap)
--forte-date-picker-trigger-radiusField corner radiusvar(--forte-radius-control)
--forte-date-picker-trigger-border-widthField border width1px
--forte-date-picker-trigger-bgField background (reset per variant)var(--forte-color-background)
--forte-date-picker-trigger-bg-hoverField background on hovervar(--forte-color-panel-hover)
--forte-date-picker-trigger-bg-activeField background while pressed or openvar(--forte-color-panel-active)
--forte-date-picker-trigger-fgField text colourvar(--forte-color-foreground)
--forte-date-picker-trigger-border-colorField border colour (reset per variant)var(--forte-color-border)
--forte-date-picker-icon-colorColour of the calendar glyphvar(--forte-color-foreground-subtle)

The popup's surface is not restyled by this component. It is a Popover.Popup, keeps data-forte="popover-popup" and every --forte-popover-* knob, and the only thing the picker does to it is step the surface padding down — a calendar arrives with its own measures, and a full --forte-surface-p around them reads as a frame rather than a margin. The column inside it is this component's own part, data-forte="date-picker-panel", which is what you target to reach this popup rather than every popover on the page.

DatePicker.Clear is a Button and tags itself data-forte="button", the way every composed component in the library does — scope it as [data-forte="date-picker-footer"] [data-forte="button"].

API reference

DatePicker.Root

Props for DatePickerRoot
PropTypeDefaultDescription
closeOnSelectbooleanClose the popup when a day is picked. Left unset it follows the mode: `single` closes on a pick, `range` closes once both ends are in, and `multiple` stays open because picking a second day would otherwise cost another two clicks.
defaultOpenbooleanWhether the popup is open when the picker first mounts.
defaultSelectedDate | CalendarRange | Date[] | nullThe selection an uncontrolled picker starts with.
formatValue((selection: CalendarSelection<M> | null, locale: string) => string | null)Replaces the trigger's text entirely. Receives the current selection and the locale, and returns `null` to fall back to the placeholder.
localestringen-USBCP 47 tag for the calendar and for the trigger's own text. Pinned rather than left to the runtime for the same reason as on `Calendar`: an unpinned locale differs between the server render and the browser.
modalboolean | "trap-focus"falseWhether the popup takes the page over while it is open. Forwarded to `Popover.Root`.
modeCalendarModesingleHow many days can be picked. Decides what `selected` holds and, unless `closeOnSelect` says otherwise, when the popup closes.
onOpenChange((open: boolean) => void)Called when the popup wants to open or close.
onSelect((selection: CalendarSelection<M> | null, day: Date) => void)Called with the next selection and the day that was clicked.
openbooleanWhether the popup is open. Pass it with `onOpenChange` to control it.
requiredbooleanfalseKeep at least one day selected: clicking the current selection no longer clears it, and `DatePicker.Clear` is disabled.
selectedDate | CalendarRange | Date[] | nullThe current selection. Pass it with `onSelect` to control the picker; leave it off entirely to let the picker own its state.

DatePicker.Trigger

Props for DatePickerTrigger
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
closeDelaynumber0How long the popover lingers after the pointer leaves, in milliseconds. Requires `openOnHover`.
delaynumber300How long the pointer must rest on the trigger before the popover opens, in milliseconds. Requires `openOnHover`.
fullWidthbooleanfalseStretch the field to fill the width of its container.
handlePopoverHandle<unknown>Associates a detached trigger with the `Popover.Root` carrying the same handle, created once outside render with `Popover.createHandle()`.
idstringIdentifies the trigger. Also how `Popover.Root`'s `triggerId` names the active trigger in controlled multi-trigger mode.
nativeButtonbooleantrueWhether the rendered element is a real `<button>`. Set it to `false` when `render` replaces the button with something else (a `<div>`, a table row), so Base UI supplies the keyboard and role behaviour the element does not have natively.
openOnHoverbooleanfalseAlso open the popover when the trigger is hovered. This is the setting that turns an "i" info icon into the accessible alternative to a tooltip: hover reveals it for pointer users while press still reveals it for everyone else.
payloadunknownData handed to the popover when this trigger opens it, so one popup can render different content per trigger. Read it from the render-function form of `Popover.Root`'s children.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, PopoverTriggerState>Replaces the rendered `<button>` with another element or component — `render={<Button variant="outline" />}` is the common case. The trigger's own neutral styling steps aside when this is present, so the two never fight over the cascade.
sizeDatePickerSizemdSize of the field. Actual dimensions also follow the ambient `data-forte-density` setting.
variantDatePickerVariantoutlineHow much visual weight the field carries. `outline` reads as a form control, `soft` as a filled field, `ghost` as an inline affordance.

DatePicker.Value

Props for DatePickerValue
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
placeholderstringShown while nothing is selected.

DatePicker.Icon

Props for DatePickerIcon
PropTypeDefaultDescription
childrenReactNode<CalendarGlyph />Contents of the slot. Defaults to a calendar glyph.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

DatePicker.Popup

Props for DatePickerPopup
PropTypeDefaultDescription
alignAligncenterHow the popup lines up with the trigger along the chosen side.
alignOffsetnumber | OffsetFunction0Shifts the popup along the alignment axis, in pixels, or a function returning one.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullThe element the popup positions against, when it should not be the trigger. Accepts an element, a ref, a getter, or a virtual element — a text selection or a right-click point.
arrowPaddingnumber5Minimum distance, in pixels, the arrow keeps from the popup's corners before it is allowed to sit off-centre (`data-uncentered`).
backdropbooleanfalseRender a scrim behind the popup. Off by default — a popover normally leaves the page visible and usable. Turn it on with `modal` on `Popover.Root`, where the page is already inert and the scrim is what says so.
backdropClassNamestringAdditional class name(s) for the backdrop element. The popup's own `className` cannot reach it, since the backdrop is a sibling rendered inside this component. Also where `--forte-popover-backdrop-z-index` goes.
classNamestringAdditional class name(s) for the panel inside the popup. Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
collisionAvoidanceCollisionAvoidanceHow the popup reacts when it would overflow the boundary — whether it flips, shifts, or stays put.
collisionBoundaryBoundaryclipping-ancestorsThe boundary the popup tries to stay inside of.
collisionPaddingPadding5Space, in pixels, kept between the popup and the collision boundary.
containerHTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | nullWhere the portal renders. Defaults to `document.body`; point it at a container when the popup has to live inside a specific stacking or shadow root.
disableAnchorTrackingbooleanfalseStops the popup re-measuring the anchor on scroll and resize. Cheaper, but the popup drifts if the anchor moves.
keepMountedbooleanfalseKeeps the portal — and therefore the popup — in the DOM while the popover is closed. Needed when something inside must stay mounted (an iframe, a media element, uncommitted form state).
positionerClassNamestringAdditional class name(s) for the positioner element, which owns placement and `z-index`. Use it to re-stack a single popover through `--forte-popover-z-index`.
positionMethod"fixed" | "absolute"absoluteWhether the popup is positioned with `position: absolute` or `position: fixed`.
sideSidebottomWhich side of the trigger to place the popup on. Flips automatically to avoid collisions. `"inline-start"` / `"inline-end"` follow writing direction.
sideOffsetnumber | OffsetFunction8Gap between trigger and popup, in pixels, or a function returning one. When an `Arrow` is rendered this must exceed the arrow's height or the arrow overlaps the trigger; the default leaves room for the default arrow.
stickybooleanfalseKeeps the popup glued to the trigger while it scrolls out of view instead of letting it detach.

DatePicker.Calendar

Props for DatePickerCalendar
PropTypeDefaultDescription
autoFocusbooleantrueFocus the calendar's active day on mount. Use it when the calendar opens in a popover, not on a calendar sitting in the page.
captionLayoutCalendarCaptionLayoutlabelWhat sits between the arrows: a static month and year, or dropdowns for one or both. Dropdowns turn a birthday twenty years back into two clicks instead of two hundred and forty.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
defaultMonthDatethe month of the selection, or the current monthThe month an uncontrolled calendar opens on.
disabledCalendarMatcherDays that cannot be picked, as a date, a range, `{ before }` / `{ after }`, `{ dayOfWeek }`, a predicate, or an array of any of those. Disabled days stay focusable so keyboard navigation can cross them.
fixedWeeksbooleanfalseAlways render six rows, so the calendar keeps one height all year and a popover containing it never resizes as you page through it.
footerReactNodeRendered under the grid inside a `role="status"` region, so a summary like "3 nights selected" is announced as it changes.
labelsCalendarLabelsChrome strings — arrow and dropdown labels. Merged over the English defaults.
maxDateDateLatest selectable day. The upper half of `minDate`.
minDateDateEarliest selectable day. Also stops navigation and trims the year dropdown, so one prop bounds the calendar in every direction at once.
monthDateThe month on display, as any date within it. Pass it with `onMonthChange` to drive navigation yourself.
navVariantButtonVariantghostVisual weight of the two navigation arrows — any `Button` variant.
numberOfMonthsnumber1How many months to show side by side.
onMonthChange((month: Date) => void)Called with the first day of the new leading month whenever navigation moves.
pagedNavigationbooleanfalseMove a whole page at a time — with `numberOfMonths={2}`, the arrows jump two months instead of one.
showOutsideDaysbooleantrueFill the first and last rows with the neighbouring months' days instead of leaving them blank.
showWeekNumbersbooleanfalseAdd a leading column of ISO 8601 week numbers.
sizeCalendarSizemdCell size and text size. Follows the ambient `data-forte-density` too.
todayDateToday's date. Override it to pin the "today" marker in tests or stories.
weekStartsOn0 | 1 | 2 | 3 | 4 | 5 | 60First column of the week — 0 is Sunday, 1 Monday, 6 Saturday. Not derived from `locale`: `Intl` cannot be asked for it in every browser, and a silent fallback would move the columns under some of your users and not others.

DatePicker.Footer

Props for DatePickerFooter
PropTypeDefaultDescription
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.

DatePicker.Clear

Props for DatePickerClear
PropTypeDefaultDescription
childrenReactNodeClearContents of the button.
classNamestringAdditional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`.
focusableWhenDisabledbooleantrue while `loading`, otherwise falseKeep the button focusable while it is disabled. A native `disabled` button is blurred by the browser, which drops focus to `<body>` and loses the user's place in the tab order — and takes `aria-busy` and `loadingLabel` out of earshot with it. Left unset, this turns itself on for the duration of `loading` so the busy state is actually announced; pass it explicitly to override.
fullWidthbooleanfalseStretch the button to fill the width of its container.
iconOnlybooleanfalseRender as a square button sized for a single icon. Enforces the 24px minimum hit target from WCAG SC 2.5.8. Always pair with `aria-label`.
loadingbooleanfalseShow a busy indicator and block interaction. The label keeps its space so the button cannot resize mid-interaction.
loadingLabelstringLoadingAnnounced to assistive technology while `loading` is true. Without it, a screen reader user gets no signal that anything is happening.
sizeButtonSizemdSize of the button. Actual dimensions also follow the ambient `data-forte-density` setting.
toneButtonToneprimaryWhich semantic colour set the button draws from. Combines freely with `variant` — `tone="danger" variant="outline"` is a low-emphasis destructive action.
variantButtonVariantsolidHow much visual weight the button carries.