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
GrDatePickerside 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
| Need | Take |
|---|---|
| A single date | GrDatePicker |
| A date with a time | GrDateTimePicker |
| A time only | GrTimePicker |
| The month grid without a field | GrCalendar |
| The period is chosen only from ready ones, a custom one is not needed | GrSegmented |
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;
maxkeeps the future out.value-adapter="isoDate"makes each border a «2026-09-07» string — the report API’s query parameters — andnamesends them as two hidden fields.Revenue reportGET /v1/reports/revenue?from=2026-09-07&to=2026-10-06A leave request with company rules
min-rangeandmax-rangebelong to the pair, not to a date: neither border says «two weeks at most». Weekends and public holidays aredisabled-dates, so a request neither starts nor ends on one, and the page counts the working days in between.A maintenance window to the quarter hour
enable-timeadds hours and minutes to both borders, andminute-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 unlessuse-12-hourssays 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
| Prop | Type | default | Description |
|---|---|---|---|
modelValuev-model | readonly [TItem, TItem] | null | undefined | undefined | Both bounds or nothing: a half-empty range does not count as a value. |
valueAdapter |
| undefined | How the bounds are emitted and come back. The adapter is applied to each one separately, so the model is a pair `[T, T]`. |
min | Date | undefined | undefined | The earliest date that can be selected. Days before it are visible in the panel but disabled. |
max | Date | undefined | undefined | The latest date that can be selected. Days after it are visible in the panel but disabled. |
disabledDates | readonly Date[] | ((date: Date) => boolean) | undefined | undefined | Forbidden dates: a list or a predicate. |
enableTime | boolean | undefined | false | The 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. |
minuteStep | number | undefined | 1 | Step of the minutes column, in minutes. `1` by default; meaningful with `enableTime`. |
secondStep | number | undefined | 1 | Step of the seconds column, in seconds. `1` by default; meaningful with `enableSeconds`. |
enableSeconds | boolean | undefined | false | A seconds column in the panel and seconds in the field. Off by default. |
use12Hours | boolean | undefined | undefined | Not set — taken from the locale. |
minRange | number | undefined | undefined | The smallest period length in days, counting both bounds. |
maxRange | number | undefined | undefined | The greatest period length in days, counting both bounds. |
presets | readonly GrDateRangePreset[] | undefined | undefined | Ready-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. |
weekStart | IsoWeekday | undefined | undefined | First day of the week per ISO (1 is Monday). Not set — from the locale. |
showWeekNumbers | boolean | undefined | undefined | A column of ISO 8601 week numbers in the panel. Not set — the `GrCalendar` setting from `GrConfigProvider`, otherwise `false`. |
today | Date | undefined | undefined | What counts as today. Set for the sake of reproducible tests. |
format | Intl.DateTimeFormatOptions | undefined | undefined | How the bounds look in the field — via `Intl` options, not a pattern string. |
separator | string | undefined | " — " | The separator between the bounds in the field. |
placeholder | string | undefined | undefined | Hint in an empty field. If it is not set and the field is editable (`editable`), the order of the date parts from the locale is shown. |
editable | boolean | undefined | false | The period can be typed by hand as a single string. The order of the parts and the separators come from the locale; what separates the bounds themselves is of no concern to parsing. |
applyOnBlur | boolean | undefined | true | What was typed is committed on blur, not only on `Enter`. |
clearable | boolean | undefined | undefined | A clear button in the field: it removes the value and emits `clear`. Not set — from `GrConfigProvider`, otherwise `false`. |
openv-model | boolean | undefined | undefined | Controlled state of the panel (`v-model:open`). |
inline | boolean | undefined | false | The panel is rendered in place: no field, no popover. The model, the adapter and `name` stay the picker's — that is how `inline` differs from a bare `GrCalendar`, which speaks in tuples. |
placement | Placement | undefined | undefined | The side of the field at which the panel opens. Not set — from `GrConfigProvider`, otherwise `bottom-start`. |
teleportTo | string | HTMLElement | undefined | undefined | A pinpoint override of the panel's mount point. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The height and type size of the field on the control scale. `md` by default; taken from `GrConfigProvider`. |
locale | string | undefined | undefined | Display locale. Not set — from the application's i18n adapter. |
id | string | undefined | undefined | The field's own `id`. Not set — taken from `GrFormField`. |
name | string | undefined | undefined | Name for the native form: the bounds are submitted as two hidden fields. |
disabled | boolean | undefined | false | The field is unavailable: the panel does not open, the value does not change. Inherited from `GrFormField`. |
readonly | boolean | undefined | false | The value is visible, the panel opens, but the selection does not change. |
invalid | boolean | undefined | false | The visual and ARIA state of an error (`aria-invalid`). Inherited from `GrFormField`. |
required | boolean | undefined | false | A required field (`aria-required`). Inherited from `GrFormField`. |
loading | boolean | undefined | false | Background work: a spinner in the field and `aria-busy`. |
ariaLabel | string | undefined | undefined | The accessible name of the field when there is no visible label. Inside `GrFormField` the name comes from its `label`. |
Slots
| Slot | Type | Description |
|---|---|---|
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 |
| 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
| Event | Signature | Description |
|---|---|---|
update:modelValue | (e: "update:modelValue", value: readonly [TItem, TItem] | null): void | The new value (`v-model`); `null` — the value was removed. |
change | (e: "change", value: readonly [TItem, TItem] | null): void | The value was committed: by a selection in the panel, by typing or by clearing. |
update:open | (e: "update:open", value: boolean): void | The panel opened or closed (`v-model:open`). |
clear | (e: "clear"): void | The value was removed with the clear button; only with `clearable`. |
focus | (e: "focus", event: FocusEvent): void | Focus entered the field. |
blur | (e: "blur", event: FocusEvent): void | Focus 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 type { GrDateRangePickerConfigurableProps, GrDateRangePickerSize, GrDateRangePickerProps } from '@feugene/granularity-chrono/components/GrDateRangePicker'Filedist/types/components/GrDateRangePicker/config.d.ts
const grDateRangePickerConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrDateRangePicker">;Filedist/types/components/GrDateRangePicker/defaults.d.ts
interface GrDateRangePickerConfigurableProps {
size: GrDateRangePickerSize;
clearable: boolean;
placement: UseFloatingPlacement;
}Filedist/types/components/GrDateRangePicker/grDateRangePickerStyles.d.ts
type GrDateRangePickerSize = GrPickerSize;Filedist/types/components/GrDateRangePicker/safelist.d.ts
const grDateRangePickerSafelist: string[];Filedist/types/components/GrDateRangePicker/GrDateRangePicker.vue.d.ts
interface GrDateRangePickerProps<T = Date> {
modelValue?: readonly [T, T] | null;
valueAdapter?: GrChronoAdapterName | GrChronoAdapter<T>;
min?: Date;
max?: Date;
disabledDates?: readonly Date[] | ((date: Date) => boolean);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;
}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:
<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:
<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.
Props in this example