GrDateRangePicker

  • chrono
  • chrono
  • API · 36 props
import { GrDateRangePicker } from '@feugene/granularity-chrono/components/GrDateRangePicker'

Overview

When to take it

  • a report for a period — “from — to” is one field and one value rather than two independent ones: two GrDatePicker side by side do not prevent choosing an end earlier than the start;
  • booking and occupancy — the preview on hover shows the future range until the second click;
  • filtering a list by dates — the model is a pair, so it travels into the query string as a single parameter;
  • the bounds have to be consistent — the component keeps the order itself and does not let an inverted period out.

When to take something else

NeedTake
A single dateGrDatePicker
A date with a timeGrDateTimePicker
A time onlyGrTimePicker
The month grid without a fieldGrCalendar
The period is chosen only from ready ones, a custom one is not neededGrSegmented

Examples 3 each with a live demo and its code

  • The period of a report

    Both borders or nothing: a half-picked range is not a value. The presets are functions, so «Last 30 days» is counted at the click; max keeps the future out. value-adapter="isoDate" makes each border a «2026-09-07» string — the report API’s query parameters — and name sends them as two hidden fields.

    Revenue report
    GET /v1/reports/revenue?from=2026-09-07&to=2026-10-06
  • A leave request with company rules

    min-range and max-range belong to the pair, not to a date: neither border says «two weeks at most». Weekends and public holidays are disabled-dates, so a request neither starts nor ends on one, and the page counts the working days in between.

    Request leave17 days left this year

    Up to 14 days in one request

    5 working days
  • A maintenance window to the quarter hour

    enable-time adds hours and minutes to both borders, and minute-step="15" keeps the announced window round — a field that allows 02:07 invites a promise nobody keeps. The 24-hour clock comes from the locale unless use-12-hours says otherwise.

    Planned maintenance · invoices APIShown on the status page and in the customer emails
    2 h 30 min of downtime

API generated from the package · cannot drift

Props

PropTypedefaultDescription
modelValuev-modelreadonly [TItem, TItem] | null | undefinedundefinedBoth bounds or nothing: a half-empty range does not count as a value.
valueAdapter
"date" | "isoDate" | "isoDateTime" | "timestamp" | GrChronoA"date" | "isoDate" | "isoDateTime" | "timestamp" | GrChronoAdapter<TItem> | undefined
undefinedHow the bounds are emitted and come back. The adapter is applied to each one separately, so the model is a pair `[T, T]`.
minDate | undefinedundefinedThe earliest date that can be selected. Days before it are visible in the panel but disabled.
maxDate | undefinedundefinedThe latest date that can be selected. Days after it are visible in the panel but disabled.
disabledDatesreadonly Date[] | ((date: Date) => boolean) | undefinedundefinedForbidden dates: a list or a predicate.
enableTimeboolean | undefinedfalseThe period bounds are set down to the minute rather than to midnight. The panel gets two time columns — for the start and for the end. The order is free: dates are picked by clicks, the time is edited whenever. There is deliberately no four-step wizard here — it would bring hidden state, "which step are we on", out of which someone who misclicked cannot be led except by going through it all again.
minuteStepnumber | undefined1Step of the minutes column, in minutes. `1` by default; meaningful with `enableTime`.
secondStepnumber | undefined1Step of the seconds column, in seconds. `1` by default; meaningful with `enableSeconds`.
enableSecondsboolean | undefinedfalseA seconds column in the panel and seconds in the field. Off by default.
use12Hoursboolean | undefinedundefinedNot set — taken from the locale.
minRangenumber | undefinedundefinedThe smallest period length in days, counting both bounds.
maxRangenumber | undefinedundefinedThe greatest period length in days, counting both bounds.
presetsreadonly GrDateRangePreset[] | undefinedundefinedReady-made periods in the panel footer: "Today", "Last 7 days". The bounds can be given as a function — "last 7 days" is counted from today, not from the day the prop was declared. The function must be pure: it is called both to decide whether the shortcut is available and to apply it. A period outside `min`/`max`, touching `disabledDates` or breaking `minRange`/`maxRange` arrives disabled: a button that does nothing lies.
weekStartIsoWeekday | undefinedundefinedFirst day of the week per ISO (1 is Monday). Not set — from the locale.

Slots

SlotTypeDescription
day{ cell: CalendarCell; selected: boolean; }A custom day cell instead of the number.
header{ title: string; goToPeriod: (delta: number) => void; }A custom grid header instead of the title and the arrows.
weekday{ label: string; full: string; isoWeekday: IsoWeekday; }A custom weekday header cell instead of the abbreviated day name.
footer
{ setRange: (from: Date, to: Date) => boolean; canSetRange: { setRange: (from: Date, to: Date) => boolean; canSetRange: (from: Date, to: Date) => boolean; close: () => void; }
The panel footer. Picking a period is delegated inward: a shortcut must set **both** bounds and must not bypass `minRange`/`maxRange`, `min`/`max` and `disabledDates`, while from outside these rules are invisible.

Events

EventSignatureDescription
update:modelValue(e: "update:modelValue", value: readonly [TItem, TItem] | null): voidThe new value (`v-model`); `null` — the value was removed.
change(e: "change", value: readonly [TItem, TItem] | null): voidThe value was committed: by a selection in the panel, by typing or by clearing.
update:open(e: "update:open", value: boolean): voidThe panel opened or closed (`v-model:open`).
clear(e: "clear"): voidThe value was removed with the clear button; only with `clearable`.
focus(e: "focus", event: FocusEvent): voidFocus entered the field.
blur(e: "blur", event: FocusEvent): voidFocus left the field.

Types

Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.

Import
import type { GrDateRangePickerConfigurableProps, GrDateRangePickerSize, GrDateRangePickerProps } from '@feugene/granularity-chrono/components/GrDateRangePicker'

Filedist/types/components/GrDateRangePicker/config.d.ts

grDateRangePickerConfigconst
const grDateRangePickerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDateRangePicker">;

Filedist/types/components/GrDateRangePicker/defaults.d.ts

GrDateRangePickerConfigurablePropsinterface
interface GrDateRangePickerConfigurableProps {
    size: GrDateRangePickerSize;
    clearable: boolean;
    placement: UseFloatingPlacement;
}

Filedist/types/components/GrDateRangePicker/grDateRangePickerStyles.d.ts

GrDateRangePickerSizetype
type GrDateRangePickerSize = GrPickerSize;

Filedist/types/components/GrDateRangePicker/safelist.d.ts

grDateRangePickerSafelistconst
const grDateRangePickerSafelist: string[];

Filedist/types/components/GrDateRangePicker/GrDateRangePicker.vue.d.ts

GrDateRangePickerPropsinterface
interface GrDateRangePickerProps<T = Date> {
    modelValue?: readonly [T, T] | null;
    valueAdapter?: GrChronoAdapterName | GrChronoAdapter<T>;
    min?: Date;
    max?: Date;
    disabledDates?: readonly Date[] | ((date: Date) => boolean);
GrDateRangePickerEmitsinterface
interface GrDateRangePickerEmits<T = Date> {
    (e: 'update:modelValue', value: readonly [T, T] | null): void;
    (e: 'change', value: readonly [T, T] | null): void;
    (e: 'update:open', value: boolean): void;
    (e: 'clear'): void;
    (e: 'focus', event: FocusEvent): void;
    (e: 'blur', event: FocusEvent): void;
}
GrDateRangePresetinterface
interface GrDateRangePreset {
    label: string;
    range: readonly [Date, Date] | (() => readonly [Date, Date]);

Usage from the library prose

The ready periods live inside the panel

presets draws a row of shortcuts in the footer: “Today”, “The last 7 days”, “This month”. The bounds can be set with a function — “the last 7 days” are counted from today rather than from the day the prop was declared.

A neighbouring GrSegmented is unsuitable for that, although it looks alike. A period shortcut is obliged to set both bounds and not to bypass minRange/maxRange, min, max and disabledDates — that is, to know the insides of the picker. Those rules are not visible from the outside, so a period that cannot be chosen is indistinguishable from an allowed one out there; inside it arrives switched off.

A footer of your own is the footer slot; it replaces the row as a whole and receives setRange, canSetRange and close, that is, exactly the same rules.

A period down to the minute

enable-time adds two columns of time to the panel — for the start and for the end:

vue
<GrDateRangePicker v-model="window" enable-time />

A fresh period gets a whole day: 00:00 and 23:59. Two midnights would look symmetric, but “from 1 to 3 August” in human terms includes the whole of the third day, while [1 Aug 00:00, 3 Aug 00:00) would silently cut off almost all of the third — the classic error of reports. With enable-seconds the end gets 23:59:59. Both values are visible and editable.

The end is put onto the same grid as the columns: with minute-step="15" that is 23:45 rather than 23:59. Otherwise the minutes would have no selected option — the end of the period could not be read where it is edited. With a step of a minute, that is, by default, it is exactly 23:59.

There is no four-step wizard here. The dates are chosen in two clicks, as without a time; the time is edited whenever and in any order. A step-by-step choice would introduce a hidden “which step are we on” state, out of which someone who mis-clicked cannot be led except by going through everything again.

The panel does not close on the second date when the time is switched on: the choice does not end there, and closing would take away from the user the very columns they switched enable-time on for.

Inside one day the order of the edges is held by the time alone. An edit after which the end would turn out earlier than the start is not applied and is announced — by the same device with which an inadmissible length is rejected: the user simply mis-clicked, and there is nothing to reset their choice for.

minRange and maxRange are still counted in days. Changing the unit of existing props would mean breaking someone else’s code; a limit in minutes is a separate minDuration/maxDuration, and they will appear when they are needed rather than “just in case”.

Typing

editable switches on the entry of a period as a single string:

vue
<GrDateRangePicker v-model="period" editable />

The string is parsed not by a separator but by the count of groups of digits: they are divided in half, and each half is parsed as a bound. 08/12/2026 — 08/14/2026, 08/12/2026 - 08/14/2026 and 08/12/2026 08/14/2026 are therefore one and the same, and en-CA with hyphens inside the dates themselves (2026-08-12 - 2026-08-14) does not break: a list of separators would fall apart at the very first locale, and there is none here.

A half of the day that turned out between the bounds (3:30 PM — 8/14/2026) goes to the bound the locale assigns it to: in en-US it stands after the hour, in ko before it.

One date instead of two is rejected. One date is not a period; completing the second bound would mean inventing on the user’s behalf what they did not set.

The reverse order is normalised, as with clicks: a period can be led backwards.

A time is accepted exactly when the picker shows it. With enable-time both bounds are obliged to carry a time, without it neither does: a picker that silently swallowed 14:30 would show a value that was not entered.

The panel follows the typing. The first bound that has been typed is highlighted as the start of the period — by the same state as after the first click in the grid; the second one that has been typed closes the band. The grid moves to the month of what was typed. The model does not change in the process.

The period is checked by the same rules as a choice with the mouse: forbidden dates, min, max, minRange, maxRange. A period that parsed but is inadmissible is not applied and is announced — the user typed everything correctly, and silence would look like a lost Enter.

A successful entry removes the start of the period if it had been opened with a click in the panel: otherwise the next click would close a period from a date the field no longer says anything about.

As in GrDateTimePicker, a field with editable shows the bounds in digits — the typing has to read back what is shown — there is no mask, and the default placeholder shows both bounds in full.

The adapter applies to every bound

The model is a pair, and valueAdapter parses and assembles both of its sides. With valueAdapter="isoDate" what goes out is ['2026-08-10', '2026-08-14'] rather than one string with a separator: the separator would have to be chosen on the consumer’s behalf, and it would break at the first locale where a dash means something else.