Vanilla TypeScript · Floating UI

Date & range picker for the web

Interactive reference for dateFlow — single dates, ranges, time selection, locales, and theming. No framework required.

Try the live demo npm install dateflow

Quick start

Minimal setup

A date-only picker with a floating calendar — focus the field to open it, copy the source on the right.

Live preview

Quick start

Date picker

Default date-only mode. Focus the field to open the calendar.

Source
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Calendar picker</title>
  </head>
  <body>
    <label for="date-field">Pick a date</label>
    <input type="text" id="date-field" />

    <script type="module">
      import { dateFlow } from "dateflow";
      import "dateflow/style.css";

      const input = document.getElementById("date-field");
      dateFlow(input, {
        value: new Date(2026, 5, 8),
        onChange: (date) => console.log("Selected:", date),
      });
    </script>
  </body>
</html>

Getting started

Pass a text input element, an id selector (#my-id), or a class selector (.my-class) to attach one or more pickers.

Basic picker with time

Pass your text input to dateFlow. Focus it to open the calendar panel.

import { dateFlow } from "dateflow";

const input = document.getElementById("date-field");
const picker = dateFlow(input, {
  value: new Date(2026, 3, 14, 12, 30),
  showTime: true,
});

Id selector

Pass #my-id instead of an element — one picker for that input.

<input type="text" id="trip-date" />

dateFlow("#trip-date", {
  value: new Date(2026, 3, 14),
});

Class selector (multiple)

Pass .my-class to attach a picker to every matching input. Returns an array of instances.

<input type="text" class="booking-date" />
<input type="text" class="booking-date" />

const pickers = dateFlow(".booking-date", {
  value: new Date(2026, 3, 14),
});

Runtime setOptions

Update locale, time, or mode without destroying the picker.

const picker = dateFlow(input, {
  onChange,
});

picker.setOptions({
  locale: de,
});
picker.setOptions({
  showTime: true,
});

picker.setOptions({
  mode: "range",
  range: { start: picker.getValue(), end: null },
});

picker.setOptions({
  mode: "single",
  value: picker.getRange().start,
});

Reset button

showResetButton adds a header icon that clears the selection.

dateFlow(input, {
  showResetButton: true,
  resetInputLabel: "Reset",
  value: new Date(2026, 3, 13, 14, 15),
  showTime: true,
});

Instance API

Read live state from the picker instance — selected dates and the year(s) shown in the calendar view.

selectedDates — single mode

{ selectedDate: Date | null }

const picker = dateFlow(input, {
  value: new Date(2026, 3, 14),
});

picker.selectedDates;
// → { selectedDate: Date }

selectedDates — range mode

{ start: Date | null, end: Date | null } — with showTime, each date includes the selected time.

const picker = dateFlow(input, {
  mode: "range",
  showTime: true,
  outputFormat: "yyyy-MM-dd HH:mm",
  range: {
    start: new Date(2026, 3, 5, 9, 0),
    end: new Date(2026, 3, 18, 17, 30),
  },
});

picker.selectedDates;
// → { start: Date, end: Date }

currentYear — single mode

{ currentYear: number } — updates when you change month or year.

picker.currentYear;
// → { currentYear: 2026 }

currentYear — range mode

{ startYear: number, endYear: number } — one year per calendar pane.

picker.currentYear;
// → { startYear: 2026, endYear: 2026 }

changeMonth() method

picker.changeMonth(months, relative = true)

  • months — when relative is true, offset to add to the current view month (negative values go back). When false, absolute month index in the current view year: 0 = January … 11 = December.
  • relative — optional, default true. true shifts by months; false jumps to month index months.

Demo starts on June (5). Open the calendar, then try the buttons.

// relative: true — shift by months
picker.changeMonth(1);   // June (5) → July (6)
picker.changeMonth(-1);  // June (5) → May (4)
picker.changeMonth(0);  // June (5) → June (5)

// relative: false — jump to month index
picker.changeMonth(0, false); // June (5) → January (0)

clear() method

picker.clear() — clears the current selection. Same behavior as the header reset button when showResetButton is enabled.

const picker = dateFlow(input, {
  value: new Date(2026, 3, 14),
  showResetButton: true,
});

picker.clear();
// selectedDates → { selectedDate: null }
// input cleared, onChange(null) fired

setDate() method

picker.setDate(newDate, format?, silent?) — set the selection programmatically from Date instances or strings. Updates the calendar view and value input; optionally fires onChange (single) or onRangeChange (range).

  • newDate — array of dates to apply. Single mode uses the first entry; range mode uses start (newDate[0]) and optional end (newDate[1]). Pass an empty array to clear the selection. Each entry may be a Date or a string parsed with format.
  • format — optional date-fns pattern for string entries. Defaults to the calendar’s output format (outputFormat, or the built-in default for the current time settings). Provide this when your strings use a different pattern than the picker displays.
  • silent — optional, default false. When true, updates the picker without invoking onChange / onRangeChange.

Two pickers below (single and range). Use the buttons — the event log records callback invocations; silent calls update the selection without adding a log entry.

// Single mode — Date object
picker.setDate([new Date(2026, 9, 20)]);

// Single mode — string (matches calendar outputFormat)
picker.setDate(["2026-10-01"]);

// String in a different format — pass format as 2nd argument
picker.setDate(["01/10/2026"], "dd/MM/yyyy");

// Clear selection
picker.setDate([]);

// Update without firing onChange
picker.setDate([new Date(2026, 11, 25)], undefined, true);

// Range mode — Date objects
picker.setDate([
  new Date(2026, 4, 1),
  new Date(2026, 4, 15),
]);

// Range mode — strings with custom format
picker.setDate(["01.06.2026", "30.06.2026"], "dd.MM.yyyy");

// Range — start only (end cleared)
picker.setDate([new Date(2026, 7, 10)]);

// Silent range update
picker.setDate(
  [new Date(2026, 0, 1), new Date(2026, 0, 7)],
  undefined,
  true,
);
Single mode
Range mode

open() and close() methods

picker.open() shows the calendar panel without focusing the input; picker.close() hides it. Calling either when already in that state is a no-op. Both work with the default popover behavior (Floating UI positioning).

const picker = dateFlow(input, {
  value: new Date(2026, 3, 14),
});

picker.open();  // show calendar
picker.close(); // hide calendar

Localization

Pass a full locale object or merge partial overrides. When firstDayOfWeek is omitted, week start is resolved from Intl.Locale.prototype.getWeekInfo() using the locale tag (or the browser), with Monday as fallback.

German locale

German month and weekday labels. Week start follows Intl for localeTag: "de" (typically Monday).

import { dateFlow } from "dateflow";
import { de } from "dateflow/locales";

dateFlow(input, {
  showWeekNumbers: true,
  locale: de,
});

French locale

French labels with week start resolved from Intl for fr.

import { dateFlow } from "dateflow";
import { fr } from "dateflow/locales";

dateFlow(input, {
  locale: fr,
});

Partial locale override

Merge over defaults — Czech labels, custom week-number header, and localized Apply/Cancel with time selection.

import { dateFlow } from "dateflow";
import { cs } from "dateflow/locales";

dateFlow(input, {
  showTime: true,
  showWeekNumbers: true,
  locale: {
    ...cs,
    weekNumberHeader: "Tý",
    rangeApply: "Potvrdit",
    rangeCancel: "Zpět",
  },
});

First day of week override

Override Intl week info with firstDayOfWeek — here English labels with a Sunday-first grid (0).

import { dateFlow } from "dateflow";
import { en } from "dateflow/locales";

dateFlow(input, {
  showWeekNumbers: true,
  locale: {
    ...en,
    firstDayOfWeek: 0,
  },
});

Constraining dates

Limit which calendar days can be selected.

Min and max date

Only days within the inclusive range are selectable.

dateFlow(input, {
  value: new Date(2026, 3, 14),
  minDate: new Date(2026, 3, 10),
  maxDate: new Date(2026, 3, 25),
});

Disabled dates (array)

Block specific dates — useful for holidays or closures. By default disabled days are dimmed only; pass disabledDatesStrikeThrough: true to add a strike-through.

dateFlow(input, {
  value: new Date(2026, 3, 14),
  disabledDates: [
    new Date(2026, 3, 17),
    new Date(2026, 3, 18),
    new Date(2026, 3, 19),
  ],
});

Disabled dates strike-through

Same blocked dates with disabledDatesStrikeThrough: true. Text color stays the same as the default disabled style.

dateFlow(input, {
  value: new Date(2026, 3, 14),
  disabledDatesStrikeThrough: true,
  disabledDates: [
    new Date(2026, 3, 17),
    new Date(2026, 3, 18),
    new Date(2026, 3, 19),
  ],
});

Disabled dates (predicate)

Disable weekends with a function.

dateFlow(input, {
  disabledDates: (date) => {
    const day = date.getDay();
    return day === 0 || day === 6;
  },
});

Allowed dates only (array)

Whitelist mode — only listed dates are selectable.

const allowed = [5, 12, 19, 26].map(
  (day) => new Date(2026, 3, day),
);

dateFlow(input, {
  value: new Date(2026, 3, 12),
  enabledDatesOnly: allowed,
});

Allowed dates only (predicate)

Only odd-numbered calendar days are selectable.

dateFlow(input, {
  enabledDatesOnly: (date) => date.getDate() % 2 === 1,
});

Time selection

Optional hour and minute selectors; 12-hour mode and seconds are supported.

24-hour time

Hour 0–23 and minute in 1-minute steps (customize with minuteStep).

dateFlow(input, {
  showTime: true,
  minuteStep: 1,
  value: new Date(2026, 3, 20, 14, 45),
  // default outputFormat: "yyyy-MM-dd HH:mm"
});

12-hour time

Hour 1–12 with an AM/PM selector.

dateFlow(input, {
  showTime: true,
  use12HourTime: true,
  value: new Date(2026, 6, 9, 15, 30),
  // default outputFormat: "yyyy-MM-dd hh:mm a"
});

Seconds (24-hour)

showSeconds adds a seconds dropdown when showTime is enabled.

dateFlow(input, {
  showTime: true,
  showSeconds: true,
  value: new Date(2026, 3, 15, 14, 30, 45),
});

Seconds (12-hour)

Hour, minute, second, and AM/PM selectors together.

dateFlow(input, {
  showTime: true,
  showSeconds: true,
  use12HourTime: true,
  value: new Date(2026, 3, 15, 15, 30, 45),
});

Dropdown-only time fields

Set allowTimeInput to false to disable typing into hour, minute, and second selectors.

dateFlow(input, {
  showTime: true,
  allowTimeInput: false,
  value: new Date(2026, 2, 20, 14, 45),
});

Direct text input

Typed input on the value field (allowInput) and in the calendar year control. By default the picker input is read-only.

allowInput — date

Type a date using outputFormat. Blur or Enter commits; Enter closes the calendar by default.

dateFlow(input, {
  allowInput: true,
  outputFormat: "yyyy-MM-dd",
  value: new Date(2026, 3, 14),
});

allowInput — 24-hour datetime

Default outputFormat includes time when showTime is true.

dateFlow(input, {
  allowInput: true,
  showTime: true,
  value: new Date(2026, 3, 15, 14, 30),
  // outputFormat: "yyyy-MM-dd HH:mm"
});

allowInput — 12-hour datetime

Use use12HourTime for AM/PM parsing and display.

dateFlow(input, {
  allowInput: true,
  showTime: true,
  use12HourTime: true,
  value: new Date(2026, 3, 15, 15, 30),
  // outputFormat: "yyyy-MM-dd hh:mm a"
});

Keep calendar open on Enter

keepOpenOnAllowInputEnter keeps the panel open after a successful Enter commit.

dateFlow(input, {
  allowInput: true,
  keepOpenOnAllowInputEnter: true,
  showTime: true,
  value: new Date(2026, 3, 15, 14, 30),
});

Year text input

Type a year directly; values are clamped to minDate and maxDate when set.

dateFlow(input, {
  minDate: new Date(2026, 3, 1),
  maxDate: new Date(2026, 8, 30),
  value: new Date(2026, 5, 1),
});

Date ranges

Range mode with optional separate start/end times and custom output formatting.

Basic range

First click starts a range, second completes it. Use onRangeChange for updates.

dateFlow(input, {
  mode: "range",
  range: {
    start: new Date(2026, 3, 5),
    end: new Date(2026, 3, 18),
  },
  outputFormat: "yyyy-MM-dd",
});

Range + reset button

showResetButton adds a header icon that clears the range and closes the picker.

dateFlow(input, {
  mode: "range",
  showResetButton: true,
  resetInputLabel: "Reset",
  range: {
    start: new Date(2026, 3, 5),
    end: new Date(2026, 3, 18),
  },
  outputFormat: "yyyy-MM-dd",
});

Range with start & end time

Separate time controls under each calendar pane in range mode.

dateFlow(input, {
  mode: "range",
  showTime: true,
  range: {
    start: new Date(2026, 3, 10, 9, 30),
    end: new Date(2026, 3, 14, 17, 45),
  },
  outputFormat: "yyyy-MM-dd HH:mm",
});

Range separator & format

rangeOutputSeparator and outputFormat shape the value input string.

dateFlow(input, {
  mode: "range",
  showTime: true,
  outputFormat: "dd.MM.yyyy HH:mm",
  rangeOutputSeparator: " → ",
  range: {
    start: new Date(2026, 8, 1, 8, 0),
    end: new Date(2026, 8, 7, 20, 0),
  },
});

Range + week numbers

Combine range mode with ISO week column.

dateFlow(input, {
  mode: "range",
  showWeekNumbers: true,
  locale: { weekNumberHeader: "KW" },
  range: {
    start: new Date(2026, 0, 6),
    end: new Date(2026, 0, 20),
  },
});

Range + dark theme

theme: "dark" sets data-cal-theme on the calendar root.

dateFlow(input, {
  mode: "range",
  theme: "dark",
  range: {
    start: new Date(2026, 4, 12),
    end: new Date(2026, 4, 26),
  },
});

Range presets

Shortcut list for common ranges. Click a preset to preselect dates; Apply commits. Hidden on mobile (≤899px).

dateFlow(input, {
  mode: "range",
  showTime: true,
  rangePresets: {
    position: "left",
    presets: [
      {
        caption: "Next 7 Days",
        start: new Date(2026, 5, 18),
        end: new Date(2026, 5, 24),
      },
      {
        caption: "Last Month",
        start: new Date(2026, 4, 18),
        end: new Date(2026, 5, 18),
      },
    ],
  },
});

Range presets — right

Set rangePresets.position to "right" to place the shortcut list on the right.

dateFlow(input, {
  mode: "range",
  rangePresets: {
    position: "right",
    presets: [/* … */],
  },
});

Formatting

Control how selected values appear in the input using date-fns patterns.

Custom outputFormat

outputFormat is a date-fns pattern synced to the value input.

dateFlow(input, {
  showTime: true,
  outputFormat: "EEEE, d MMMM yyyy 'at' HH:mm",
  value: new Date(2026, 7, 19, 15, 0),
});

Theming

Built-in theme presets and CSS custom properties for colors, typography, and layout.

Dark theme preset

Sets data-cal-theme="dark" and bundled CSS variables.

dateFlow(input, {
  theme: "dark",
  showTime: true,
});

High-contrast preset

theme: "contrast" — strong borders and yellow accent.

dateFlow(input, {
  theme: "contrast",
});

Custom CSS variables

Override --cal-* tokens on the panel wrapper returned by getCalendarElement() — no named theme required. The input field reads a subset of the same variables when attached.

  • --cal-bg — panel and input background. panel.style.setProperty("--cal-bg", "#fff7ed")
  • --cal-surface — secondary surfaces (select menus, reset hover). panel.style.setProperty("--cal-surface", "#ffedd5")
  • --cal-text — primary text color. panel.style.setProperty("--cal-text", "#431407")
  • --cal-muted — secondary text (weekday headers, muted days). panel.style.setProperty("--cal-muted", "#9a3412")
  • --cal-input-placeholder — input placeholder text color. panel.style.setProperty("--cal-input-placeholder", "#b45309")
  • --cal-border — panel borders and dividers. panel.style.setProperty("--cal-border", "#fdba74")
  • --cal-header-select-border — month, year, and time control borders. panel.style.setProperty("--cal-header-select-border", "#fb923c")
  • --cal-accent — selected day, today, hover, links, and focus rings. panel.style.setProperty("--cal-accent", "#c2410c")
  • --cal-accent-contrast — text on accent-filled cells and buttons. panel.style.setProperty("--cal-accent-contrast", "#fff7ed")
  • --cal-radius — calendar panel corner radius. panel.style.setProperty("--cal-radius", "14px")
  • --cal-shadow — panel drop shadow. panel.style.setProperty("--cal-shadow", "0 12px 40px rgb(194 65 12 / 0.12)")
  • --cal-day-size — day cell width and height. panel.style.setProperty("--cal-day-size", "38px")
  • --cal-font — font-family stack for the calendar. panel.style.setProperty("--cal-font", 'system-ui, sans-serif')

Full example below — the same setProperty calls work on picker.getCalendarElement() in single and range mode.

const picker = dateFlow(input, {
  value: new Date(2026, 5, 1),
});
const panel = picker.getCalendarElement();

panel.style.setProperty("--cal-bg", "#fff7ed");
panel.style.setProperty("--cal-surface", "#ffedd5");
panel.style.setProperty("--cal-text", "#431407");
panel.style.setProperty("--cal-muted", "#9a3412");
panel.style.setProperty("--cal-input-placeholder", "#b45309");
panel.style.setProperty("--cal-border", "#fdba74");
panel.style.setProperty("--cal-header-select-border", "#fb923c");
panel.style.setProperty("--cal-accent", "#c2410c");
panel.style.setProperty("--cal-accent-contrast", "#fff7ed");
panel.style.setProperty("--cal-radius", "14px");
panel.style.setProperty("--cal-shadow", "0 12px 40px rgb(194 65 12 / 0.12)");
panel.style.setProperty("--cal-day-size", "38px");
panel.style.setProperty("--cal-font", 'system-ui, sans-serif');
Single mode
Range mode

Custom accent only

Override a single token — selected day, today, hover, and focus rings follow --cal-accent; everything else keeps the default theme. Works in range mode with time pickers too.

const picker = dateFlow(input, {
  mode: "range",
  showTime: true,
  range: {
    start: new Date(2026, 5, 1, 9, 30),
    end: new Date(2026, 5, 15, 17, 45),
  },
});
const panel = picker.getCalendarElement();
panel.style.setProperty("--cal-accent", "#7c3aed");

Accessibility

The calendar panel is exposed as a labeled region for assistive technologies. Navigation controls, month and time selectors, and the year field ship with default accessible names; use ariaLabel when the calendar needs a context-specific name in your UI.

ariaLabel

Sets the aria-label on the calendar root. Defaults to "Calendar" when omitted.

dateFlow(input, {
  ariaLabel: "Delivery date",
});