universal-datetime-picker

Overlay and triggers

Presentation controls whether the calendar sits in the page flow or floats. Open-state props control when it is visible.

Presentation modes

Mode How to enable Behavior
Inline inline Calendar stays in layout. Always "open". No backdrop.
Overlay (modal) Default when not inline, and popover is not set Centered panel with dimmed backdrop. Escape and outside click close it.
Popover popover + anchorEl on DateTime Positions near the anchor with position: fixed, flips when needed, repositions on scroll/resize.

DateTimeInput is always popover + its own input as anchor. DateTimeRange has no popover or anchorEl. Use inline or the centered overlay only.

Open-state props

Prop Type Effect
open boolean Controlled open state
defaultOpen boolean Uncontrolled initial open
onOpenChange (open: boolean) => void Fires when open state changes

defaultOpen is true for non-inline DateTime

For DateTime (and DateTimeRange) when inline is false, defaultOpen defaults to true. The overlay appears on mount. That surprises people building a closed custom button. Fix it with either:

  • defaultOpen={false} and open via onOpenChange / a button, or
  • fully controlled open + onOpenChange.

DateTimeInput defaults to closed because the field is the trigger.

React: DateTimeInput (built-in trigger)

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

export function FormDate() {
  const [value, setValue] = useState<Date | null>(null);

  return (
    <DateTimeInput
      mode="date"
      asString={false}
      value={value}
      onChange={(next) => setValue(next instanceof Date ? next : null)}
      placeholder="Choose a date"
    />
  );
}

React: custom button with popover

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

function CustomDateTrigger() {
  const [open, setOpen] = useState(false);
  const [value, setValue] = useState<Date | null>(null);
  const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);

  return (
    <>
      <button
        ref={setAnchorEl}
        type="button"
        onClick={() => setOpen(true)}
        aria-haspopup="dialog"
        aria-expanded={open}
      >
        {value ? value.toLocaleDateString() : "Choose a date"}
      </button>
      <DateTime
        mode="date"
        open={open}
        onOpenChange={setOpen}
        popover
        anchorEl={anchorEl}
        asString={false}
        value={value}
        onChange={(next) => setValue(next instanceof Date ? next : null)}
      />
    </>
  );
}

Omit popover and anchorEl for a centered modal overlay. Keep open controlled the same way.

Vanilla: popover + anchor + closed by default

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

const mount = document.querySelector("#picker-root");
const trigger = document.querySelector("#open-picker");
if (!mount || !trigger) throw new Error("missing mount or trigger");

const picker = createDateTimePicker(mount, {
  mode: "date",
  asString: false,
  popover: true,
  anchorEl: trigger,
  defaultOpen: false,
  onChange: (value) => console.log(value),
  onOpenChange: (isOpen) => {
    trigger.setAttribute("aria-expanded", String(isOpen));
  },
});

trigger.addEventListener("click", () => {
  picker.update({ open: true });
});

// Later: picker.destroy();

The handle exposes update, destroy, and getController. Pass defaultOpen: false, popover: true, and anchorEl, then open with update({ open: true }).

Web Components note

For non-inline custom elements, an absent open attribute means closed. That differs from React DateTime's defaultOpen=true. Set the open attribute (or toggle it from script) when you want the overlay visible. Listen for openchange to sync your trigger button.

<button id="btn" type="button">Pick date</button>
<datetime-picker id="dp" mode="date"></datetime-picker>
<script>
  const dp = document.getElementById("dp");
  const btn = document.getElementById("btn");
  btn.addEventListener("click", () => dp.setAttribute("open", ""));
  dp.addEventListener("openchange", (e) => {
    if (!e.detail) dp.removeAttribute("open");
  });
</script>

Related: Components · Date range · Vanilla · Web Components · Examples · Troubleshooting