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.
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:
mode | Closes when |
|---|---|
single | a day is picked — but not when one is cleared, where a vanishing popup leaves you unsure it took |
range | both ends are in, the only moment the answer is complete |
multiple | never — you are still choosing, and closing after the first day would cost two clicks for the second |
Picking several days
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
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
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
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.
| Key | Behaviour |
|---|---|
| Enter then Space | Opens the calendar from the field. |
| Escape | Closes the calendar and returns focus to the field. |
| Tab | Inside 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.
| Property | Controls | Default |
|---|---|---|
--forte-date-picker-trigger-height | Field height (reset per size) | var(--forte-control-h-md) |
--forte-date-picker-trigger-padding-x | Field inline padding (reset per size) | var(--forte-control-px-md) |
--forte-date-picker-trigger-font-size | Field font size (reset per size) | var(--forte-font-size-2) |
--forte-date-picker-trigger-gap | Gap between the value and the glyph | var(--forte-control-gap) |
--forte-date-picker-trigger-radius | Field corner radius | var(--forte-radius-control) |
--forte-date-picker-trigger-border-width | Field border width | 1px |
--forte-date-picker-trigger-bg | Field background (reset per variant) | var(--forte-color-background) |
--forte-date-picker-trigger-bg-hover | Field background on hover | var(--forte-color-panel-hover) |
--forte-date-picker-trigger-bg-active | Field background while pressed or open | var(--forte-color-panel-active) |
--forte-date-picker-trigger-fg | Field text colour | var(--forte-color-foreground) |
--forte-date-picker-trigger-border-color | Field border colour (reset per variant) | var(--forte-color-border) |
--forte-date-picker-icon-color | Colour of the calendar glyph | var(--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
| Prop | Type | Default | Description |
|---|---|---|---|
closeOnSelect | boolean | Close 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. | |
defaultOpen | boolean | Whether the popup is open when the picker first mounts. | |
defaultSelected | Date | CalendarRange | Date[] | null | The 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. | |
locale | string | en-US | BCP 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. |
modal | boolean | "trap-focus" | false | Whether the popup takes the page over while it is open. Forwarded to `Popover.Root`. |
mode | CalendarMode | single | How 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. | |
open | boolean | Whether the popup is open. Pass it with `onOpenChange` to control it. | |
required | boolean | false | Keep at least one day selected: clicking the current selection no longer clears it, and `DatePicker.Clear` is disabled. |
selected | Date | CalendarRange | Date[] | null | The current selection. Pass it with `onSelect` to control the picker; leave it off entirely to let the picker own its state. |
DatePicker.Trigger
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
closeDelay | number | 0 | How long the popover lingers after the pointer leaves, in milliseconds. Requires `openOnHover`. |
delay | number | 300 | How long the pointer must rest on the trigger before the popover opens, in milliseconds. Requires `openOnHover`. |
fullWidth | boolean | false | Stretch the field to fill the width of its container. |
handle | PopoverHandle<unknown> | Associates a detached trigger with the `Popover.Root` carrying the same handle, created once outside render with `Popover.createHandle()`. | |
id | string | Identifies the trigger. Also how `Popover.Root`'s `triggerId` names the active trigger in controlled multi-trigger mode. | |
nativeButton | boolean | true | Whether 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. |
openOnHover | boolean | false | Also 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. |
payload | unknown | Data 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. | |
render | ReactElement<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. | |
size | DatePickerSize | md | Size of the field. Actual dimensions also follow the ambient `data-forte-density` setting. |
variant | DatePickerVariant | outline | How much visual weight the field carries. `outline` reads as a form control, `soft` as a filled field, `ghost` as an inline affordance. |
DatePicker.Value
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
placeholder | string | Shown while nothing is selected. |
DatePicker.Icon
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | <CalendarGlyph /> | Contents of the slot. Defaults to a calendar glyph. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
DatePicker.Popup
| Prop | Type | Default | Description |
|---|---|---|---|
align | Align | center | How the popup lines up with the trigger along the chosen side. |
alignOffset | number | OffsetFunction | 0 | Shifts the popup along the alignment axis, in pixels, or a function returning one. |
anchor | Element | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null | The 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. | |
arrowPadding | number | 5 | Minimum distance, in pixels, the arrow keeps from the popup's corners before it is allowed to sit off-centre (`data-uncentered`). |
backdrop | boolean | false | Render 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. |
backdropClassName | string | Additional 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. | |
className | string | Additional class name(s) for the panel inside the popup. Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
collisionAvoidance | CollisionAvoidance | How the popup reacts when it would overflow the boundary — whether it flips, shifts, or stays put. | |
collisionBoundary | Boundary | clipping-ancestors | The boundary the popup tries to stay inside of. |
collisionPadding | Padding | 5 | Space, in pixels, kept between the popup and the collision boundary. |
container | HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null | Where 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. | |
disableAnchorTracking | boolean | false | Stops the popup re-measuring the anchor on scroll and resize. Cheaper, but the popup drifts if the anchor moves. |
keepMounted | boolean | false | Keeps 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). |
positionerClassName | string | Additional 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" | absolute | Whether the popup is positioned with `position: absolute` or `position: fixed`. |
side | Side | bottom | Which side of the trigger to place the popup on. Flips automatically to avoid collisions. `"inline-start"` / `"inline-end"` follow writing direction. |
sideOffset | number | OffsetFunction | 8 | Gap 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. |
sticky | boolean | false | Keeps the popup glued to the trigger while it scrolls out of view instead of letting it detach. |
DatePicker.Calendar
| Prop | Type | Default | Description |
|---|---|---|---|
autoFocus | boolean | true | Focus the calendar's active day on mount. Use it when the calendar opens in a popover, not on a calendar sitting in the page. |
captionLayout | CalendarCaptionLayout | label | What 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. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
defaultMonth | Date | the month of the selection, or the current month | The month an uncontrolled calendar opens on. |
disabled | CalendarMatcher | Days 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. | |
fixedWeeks | boolean | false | Always render six rows, so the calendar keeps one height all year and a popover containing it never resizes as you page through it. |
footer | ReactNode | Rendered under the grid inside a `role="status"` region, so a summary like "3 nights selected" is announced as it changes. | |
labels | CalendarLabels | Chrome strings — arrow and dropdown labels. Merged over the English defaults. | |
maxDate | Date | Latest selectable day. The upper half of `minDate`. | |
minDate | Date | Earliest selectable day. Also stops navigation and trims the year dropdown, so one prop bounds the calendar in every direction at once. | |
month | Date | The month on display, as any date within it. Pass it with `onMonthChange` to drive navigation yourself. | |
navVariant | ButtonVariant | ghost | Visual weight of the two navigation arrows — any `Button` variant. |
numberOfMonths | number | 1 | How many months to show side by side. |
onMonthChange | ((month: Date) => void) | Called with the first day of the new leading month whenever navigation moves. | |
pagedNavigation | boolean | false | Move a whole page at a time — with `numberOfMonths={2}`, the arrows jump two months instead of one. |
showOutsideDays | boolean | true | Fill the first and last rows with the neighbouring months' days instead of leaving them blank. |
showWeekNumbers | boolean | false | Add a leading column of ISO 8601 week numbers. |
size | CalendarSize | md | Cell size and text size. Follows the ambient `data-forte-density` too. |
today | Date | Today's date. Override it to pin the "today" marker in tests or stories. | |
weekStartsOn | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 0 | First 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
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. |
DatePicker.Clear
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | Clear | Contents of the button. |
className | string | Additional class name(s). Applied after the internal styles so consumer utilities (e.g. Tailwind) win without needing `!important`. | |
focusableWhenDisabled | boolean | true while `loading`, otherwise false | Keep 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. |
fullWidth | boolean | false | Stretch the button to fill the width of its container. |
iconOnly | boolean | false | Render 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`. |
loading | boolean | false | Show a busy indicator and block interaction. The label keeps its space so the button cannot resize mid-interaction. |
loadingLabel | string | Loading | Announced to assistive technology while `loading` is true. Without it, a screen reader user gets no signal that anything is happening. |
size | ButtonSize | md | Size of the button. Actual dimensions also follow the ambient `data-forte-density` setting. |
tone | ButtonTone | primary | Which semantic colour set the button draws from. Combines freely with `variant` — `tone="danger" variant="outline"` is a low-emphasis destructive action. |
variant | ButtonVariant | solid | How much visual weight the button carries. |