universal-datetime-picker

Date range

Pick a start and end date on one calendar. React: DateTimeRange. Vanilla: createDateTimeRangePicker. Web Component: <datetime-picker-range>.

Value shape

  • asString omitted or false: { start: Date | null; end: Date | null }
  • asString={true}: { start: string | null; end: string | null }

Controlled usage passes the same shape to value. Incomplete ranges may have one side still null while the user finishes the second click.

Behavior

  • First click sets start. Second click sets end and commits.
  • No OK button. Commit happens when the range is complete.
  • Hover preview highlights the tentative span.
  • Keyboard grid navigation works like the single-date calendar.
  • Non-inline defaultOpen defaults to true. Use defaultOpen={false} or controlled open for a closed trigger pattern.

Limitations

Range is date-only and intentionally smaller than DateTime:

  • No popover
  • No anchorEl
  • No theme prop (use CSS / data-ctp-theme)
  • No mode or layout
  • No use12Hours or showSeconds

Still supported: bounds, disable past/future, weekStartsOn, locale, labels, format, asString, open-state props, inline, className, style.

React example

import { useState } from "react";
import { DateTimeRange } from "universal-datetime-picker";
import "universal-datetime-picker/style.css";

type Range = { start: Date | null; end: Date | null };

export function StayDates() {
  const [range, setRange] = useState<Range>({ start: null, end: null });

  return (
    <DateTimeRange
      inline
      asString={false}
      value={range}
      onChange={setRange}
      disablePastDates
      weekStartsOn={1}
    />
  );
}

Vanilla example

import { createDateTimeRangePicker } from "universal-datetime-picker/vanilla";
import "universal-datetime-picker/style.css";

const mount = document.querySelector("#range");
if (!mount) throw new Error("#range missing");

createDateTimeRangePicker(mount, {
  inline: true,
  asString: false,
  disablePastDates: true,
  onChange: (value) => console.log(value.start, value.end),
});

Closed-by-default overlay trigger

Range overlays are centered (no popover). Keep them closed until a button opens them:

import { useState } from "react";
import { DateTimeRange } from "universal-datetime-picker";
import "universal-datetime-picker/style.css";

export function RangeModal() {
  const [open, setOpen] = useState(false);
  const [range, setRange] = useState({ start: null, end: null });

  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Select dates
      </button>
      <DateTimeRange
        open={open}
        onOpenChange={setOpen}
        asString={false}
        value={range}
        onChange={setRange}
      />
    </>
  );
}

Uncontrolled alternative: defaultOpen={false} plus a way to call into open state if your wrapper exposes it. Controlled open is the clearest pattern.

Components · Props · Overlay · Examples · Troubleshooting