# Date Range Picker

Edit and select a start and end date.

Out-of-office dates

[Complete TSX file](/examples/date-range-picker/default.tsx)

```tsx
"use client";

import { DateRangePicker } from "@/siteplane/components/date-range-picker";
import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Out of office</FieldLabel>
      <DateRangePicker
        className="w-full"
        defaultValue={{
          from: new Date(2026, 9, 12),
          to: new Date(2026, 9, 16),
        }}
      />
      <FieldDescription>
        Teammates see you as away on these dates.
      </FieldDescription>
    </Field>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/date-range-picker
```

With npm: `npx shadcn@4.18.0 add @siteplane/date-range-picker`. [Registry JSON](/registry/date-range-picker.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input
```

Field is installed with Input.

## Usage

```tsx
import { DateRangePicker } from "@/siteplane/components/date-range-picker";

<DateRangePicker aria-label="Trip dates" />;
```

## Controlled value

Pass `value` and `onValueChange` to keep the range in React state. The control reports complete ranges and `undefined` when cleared; store `null` for an empty controlled value, because `undefined` makes the picker uncontrolled.

Controlled trip dates

[Complete TSX file](/examples/date-range-picker/controlled.tsx)

```tsx
"use client";

import {
  DateRangePicker,
  type DateRange,
} from "@/siteplane/components/date-range-picker";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState<DateRange | null>({
    from: new Date(2026, 8, 7),
    to: new Date(2026, 8, 12),
  });
  return (
    <DateRangePicker
      aria-label="Trip dates"
      value={value}
      onValueChange={(next) => setValue(next ?? null)}
    />
  );
}
```

## Unavailable dates

Use `calendarProps.disabled` for dates that can't be booked and `endMonth` for the booking window. The calendar doesn't offer these dates, and a typed date that breaks a rule marks the range invalid once it loses focus or the form is submitted; `FieldError` shows the message. Check the range again on the server.

Stay dates within the booking window

[Complete TSX file](/examples/date-range-picker/constraints.tsx)

```tsx
"use client";

import { DateRangePicker } from "@/siteplane/components/date-range-picker";
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/siteplane/components/field";
import { useEffect, useState } from "react";

export default function Example() {
  // Read today in the browser once the example mounts. A page rendered on a
  // server or at build time would otherwise keep the day it was rendered.
  const [today, setToday] = useState<Date>();
  useEffect(() => setToday(new Date()), []);
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Stay dates</FieldLabel>
      <DateRangePicker
        calendarProps={{
          disabled: { before: today },
          endMonth:
            today && new Date(today.getFullYear(), today.getMonth() + 5),
        }}
        className="w-full"
      />
      <FieldDescription>Bookings open up to six months ahead.</FieldDescription>
      <FieldError />
    </Field>
  );
}
```

## Disabled, read-only and invalid

A disabled `Field` also disables the range, and `readOnly` shows a range that can't be changed on the regular field surface. For an error from your application, set `invalid` on `Field` and explain the problem in a `FieldError`.

Disabled, read-only and invalid date ranges

[Complete TSX file](/examples/date-range-picker/states.tsx)

```tsx
"use client";

import { DateRangePicker } from "@/siteplane/components/date-range-picker";
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/siteplane/components/field";

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      <Field disabled>
        <FieldLabel>Trip dates</FieldLabel>
        <DateRangePicker
          className="w-full"
          defaultValue={{
            from: new Date(2026, 10, 2),
            to: new Date(2026, 10, 6),
          }}
        />
      </Field>
      <Field>
        <FieldLabel>Billing period</FieldLabel>
        <DateRangePicker
          className="w-full"
          defaultValue={{
            from: new Date(2026, 9, 1),
            to: new Date(2026, 9, 31),
          }}
          readOnly
        />
        <FieldDescription>Set by your plan.</FieldDescription>
      </Field>
      <Field invalid>
        <FieldLabel>Out of office</FieldLabel>
        <DateRangePicker className="w-full" />
        <FieldError match={true}>Choose a start and an end date.</FieldError>
      </Field>
    </div>
  );
}
```

## Submission and reset

Set `startName` and `endName` to submit two hidden `YYYY-MM-DD` strings; the example submits `{ "departureDate": "2026-11-02", "returnDate": "2026-11-06" }`. An empty endpoint submits an empty string, and there is no single `name` or range object. Changing `locale` changes only the display. With `required`, submitting an incomplete range is blocked, the field is focused and `FieldError` shows the required message. Native form reset restores `defaultValue`; reset controlled state yourself.

Date range in a form

[Complete TSX file](/examples/date-range-picker/form.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import { DateRangePicker } from "@/siteplane/components/date-range-picker";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-4"
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field>
        <FieldLabel>Trip dates</FieldLabel>
        <DateRangePicker
          className="w-full"
          defaultValue={{
            from: new Date(2026, 10, 2),
            to: new Date(2026, 10, 6),
          }}
          endName="returnDate"
          required
          startName="departureDate"
        />
        <FieldError />
      </Field>
      <div className="flex gap-2">
        <Button type="submit">Submit</Button>
        <Button type="reset" variant="outline">
          Reset
        </Button>
      </div>
    </Form>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| aria-label | string | unset | Accessible name when no FieldLabel or aria-labelledby names the range. |
| aria-labelledby / aria-describedby | string | unset | External label and help ids; inside a Field, FieldLabel, FieldDescription and FieldError are connected automatically. |
| aria-invalid | AriaAttributes[aria-invalid] | unset | External invalid state; while set, it replaces the built-in date checks. |
| value / defaultValue | DateRange \| null (value only) | uncontrolled / unset | Controlled or initial range; null is an empty controlled value. |
| onValueChange | (value: DateRange \| undefined) => void | unset | Reports a complete range, or undefined when cleared. |
| disabled | boolean | false | Prevents editing/opening; a disabled Field also disables the range. |
| className | string | unset | Outer control styling. |
| locale | string | context | Formatting locale, not time zone. |
| calendarProps | CalendarOptions | unset | The calendar options below: defaultMonth, disabled, startMonth, endMonth and todayDate. |
| readOnly / required | boolean | false | Read-only field on the regular surface; required blocks form submission until both dates are set, focuses the field and shows the required message in FieldError. |
| size | sm \| md \| lg \| xl | provider / lg | Control size. |
| textSize | sm \| base | unset | Field text size; unset is 14px, or 16px at xl, like Input. |
| variant | default \| elevated \| inset \| soft | default | Input surface. |
| form | string | unset | Associated form ID. |
| startName / endName | string | unset | Names of the two hidden serialized date fields. |
| startLabel / endLabel | string | localized Start Date / End Date | Accessible names of the start and end date inputs. Screen readers announce each date segment with this name and the range label. |
| openPickerLabel | string | Open date range calendar | Calendar button name. |
| iconMode | action-end \| none | action-end | action-end ends the field with the button that opens the calendar; none leaves the range to typing, for a range beside a calendar of its own. |
| incompleteLabel | string | Enter a complete date range. | The error of a required picker while its range is incomplete, shown by FieldError. |

`size` and `variant` use the same scale and surfaces as Input; see [Input sizes](/docs/components/input/#sizes) and [variants](/docs/components/input/#variants).

### Calendar options

`calendarProps` takes `CalendarOptions`, the type of the same options on [Calendar](/docs/components/calendar/#calendar-options). `startMonth` and `endMonth` (Date, unset) limit the calendar to the first and last allowed months. `disabled` (unset) makes days unavailable with one matcher or a list of matchers, each a Date, a `(date: Date) => boolean` test or a rule with `after`, `before`, `from`, `to` and `dayOfWeek` (0 Sunday through 6 Saturday); `true` disables the whole calendar. Both apply to picking in the calendar and to typed dates.
`defaultMonth` (Date, unset) sets the month the calendar shows when it opens; while it is set, it also wins over the month of the selected range. `todayDate` (Date, the device's date) sets the day the calendar marks as today; without a selected range, the calendar opens on its month, and typed segments start from it.

Exported types: `DateRangePickerProps`, `DateRange`. These describe the contracts above.

[Read the installed source](/source/date-range-picker).

## Usage notes

Values use native Date calendar fields in the local runtime time zone. Locale changes formatting, not the zone. DateRange is `{from?:Date,to?:Date}`; the control commits a complete range. Inside a Field, FieldLabel names DateRangePicker and clicking it focuses the first segment; outside a Field, pass `aria-label` or `aria-labelledby`. Arrow keys change the focused date segment. Only the props listed above are supported, and DateRangePicker renders its own segments, calendar button and popover, the last two only with `iconMode="action-end"`; it accepts no children or ref. The named Props type and value type are exported from the component module.

The start date, the dash and the end date read in the direction of the locale, which the arrow keys follow too, and are isolated from the text around them. An `en-US` range stays 01/01/2024 – 10/14/2026 in a right-to-left page, while the field keeps the page's direction: its calendar button sits at the page's inline end.
