Siteplane UI

Date Range Picker

Edit and select a start and end date.

Markdown

Teammates see you as away on these dates.

"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

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

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Field is installed with Input.

Usage

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.

"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.

Bookings open up to six months ahead.

"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.

Set by your plan.

Choose a start and an end date.
"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.

"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

Component properties
PropTypeDefaultDescription
aria-labelstringunsetAccessible name when no FieldLabel or aria-labelledby names the range.
aria-labelledby / aria-describedbystringunsetExternal label and help ids; inside a Field, FieldLabel, FieldDescription and FieldError are connected automatically.
aria-invalidAriaAttributes[aria-invalid]unsetExternal invalid state; while set, it replaces the built-in date checks.
value / defaultValueDateRange | null (value only)uncontrolled / unsetControlled or initial range; null is an empty controlled value.
onValueChange(value: DateRange | undefined) => voidunsetReports a complete range, or undefined when cleared.
disabledbooleanfalsePrevents editing/opening; a disabled Field also disables the range.
classNamestringunsetOuter control styling.
localestringcontextFormatting locale, not time zone.
calendarPropsCalendarOptionsunsetThe calendar options below: defaultMonth, disabled, startMonth, endMonth and todayDate.
readOnly / requiredbooleanfalseRead-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.
sizesm | md | lg | xlprovider / lgControl size.
textSizesm | baseunsetField text size; unset is 14px, or 16px at xl, like Input.
variantdefault | elevated | inset | softdefaultInput surface.
formstringunsetAssociated form ID.
startName / endNamestringunsetNames of the two hidden serialized date fields.
startLabel / endLabelstringlocalized Start Date / End DateAccessible names of the start and end date inputs. Screen readers announce each date segment with this name and the range label.
openPickerLabelstringOpen date range calendarCalendar button name.
iconModeaction-end | noneaction-endaction-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.
incompleteLabelstringEnter 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 and variants.

Calendar options

calendarProps takes CalendarOptions, the type of the same options on Calendar. 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.

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.