DateRangeField
Pick a start and end date, with presets like Last 30 days and an optional comparison.
SourcePackage @weft-js/warp
Preview
tsx
import { DateRangeField } from '@weft-js/warp'import type { IsoDate } from '@weft-js/warp'export function Presets() { return ( <DateRangeField label="Period" today={TODAY} defaultValue={{ start: '2026-10-01', end: '2026-10-31' }} /> )}const TODAY: IsoDate = '2026-11-02'Installation
npm install @weft-js/warpUsage
tsx
import { DateRangeField } from '@weft-js/warp'<DateRangeField label="Period" today={TODAY} defaultValue={{ start: '2026-10-01', end: '2026-10-31' }} />Your own presets
Each preset is a function of today.
tsx
import { DateRangeField } from '@weft-js/warp'import type { DateRange, IsoDate } from '@weft-js/warp'export function Custom() { return ( <DateRangeField label="Report on" today={TODAY} presets={REPORTING} defaultValue={Q3} /> )}const TODAY: IsoDate = '2026-11-02'const REPORTING = [ { id: 'sprint', label: 'This sprint', range: (): DateRange => ({ start: '2026-10-26', end: '2026-11-06' }) }, { id: 'q3', label: 'Q3', range: (): DateRange => ({ start: '2026-07-01', end: '2026-09-30' }) }, { id: 'ytd', label: 'Year to date', range: (t: IsoDate): DateRange => ({ start: '2026-01-01', end: t }) },]const Q3: DateRange = { start: '2026-07-01', end: '2026-09-30' }A longest span
maxDays stops the range growing past a limit.
tsx
import { DateRangeField } from '@weft-js/warp'import type { IsoDate } from '@weft-js/warp'export function Limit() { return ( <DateRangeField label="Log export" hint="Up to 14 days at a time" today={TODAY} presets={false} months={1} maxDays={14} max={TODAY} defaultValue={{ start: '2026-10-20', end: '2026-10-27' }} /> )}const TODAY: IsoDate = '2026-11-02'Compare
compare shades the earlier period in the calendar.
tsx
import { state } from '@weft-js/core'import { DateRangeField, Stack, Text, compareRange } from '@weft-js/warp'import type { DateRange, IsoDate } from '@weft-js/warp'export function Compare() { const range = state<DateRange>({ start: '2026-10-01', end: '2026-10-31' }) const before = () => compareRange(range.value, 'previous-period') return ( <Stack gap="related"> <DateRangeField label="Period" today={TODAY} value={range} compare="previous-period" /> <Text subtle role="caption">Compared with {before()?.start} to {before()?.end}</Text> </Stack> )}const TODAY: IsoDate = '2026-11-02'Filter a list
Change the range, and the list follows.
tsx
import { state } from '@weft-js/core'import { DateRangeField, Row, Stack, Text } from '@weft-js/warp'import type { DateRange, IsoDate } from '@weft-js/warp'export function Filter() { const range = state<DateRange>({ start: '2026-10-31', end: '2026-10-31' }) const shown = () => DEPLOYS.filter(d => d.at.slice(0, 10) >= (range.value.start ?? '') && d.at.slice(0, 10) <= (range.value.end ?? '9999')) return ( <Stack gap="group"> <DateRangeField label="Deploys between" today={TODAY} value={range} months={1} max={TODAY} /> <Text role="label">{shown().length} deploys</Text> <Stack gap="related" divided> {shown().map(d => ( <Row key={d.id} gap="related" justify="between"> <Text>{d.project}</Text> <Text subtle role="caption">{d.at.slice(5).replace('T', ' at ')}</Text> </Row> ))} </Stack> </Stack> )}const DEPLOYS = [ { id: 'd-4821', project: 'storefront', branch: 'main', commit: '9f3ac21', message: 'Cache product images at the edge', author: 'Priya Raman', status: 'Live', minutes: 2, at: '2026-10-31T16:42' }, { id: 'd-4820', project: 'search-api', branch: 'ranking-v2', commit: '1be07d4', message: 'Rank exact matches first', author: 'Lena Fischer', status: 'Building', minutes: 6, at: '2026-10-31T16:10' }, { id: 'd-4819', project: 'mobile-bff', branch: 'main', commit: 'c44e190', message: 'Retry timed out upstream calls', author: 'Sam Okafor', status: 'Failed', minutes: 4, at: '2026-10-31T15:27' }, { id: 'd-4818', project: 'checkout', branch: 'main', commit: '70d2fa8', message: 'Add Apple Pay to the last step', author: 'Tomás Oliveira', status: 'Live', minutes: 3, at: '2026-10-31T14:03' }, { id: 'd-4817', project: 'edge-cache', branch: 'main', commit: 'e81b6c2', message: 'Lower the stale window to 30s', author: 'Jun Tanaka', status: 'Live', minutes: 1, at: '2026-10-31T11:55' }, { id: 'd-4816', project: 'billing', branch: 'main', commit: '2ad9e03', message: 'Prorate seats on plan change', author: 'Mara Lindqvist', status: 'Live', minutes: 3, at: '2026-10-29T17:20' },]const TODAY: IsoDate = '2026-11-02'When to use
Use it for
- Filtering a report or a list by dates
- Booking a span of days
Reach for something else
- PeriodPicker, when the range is a whole month, quarter or year
Accessibility
- Arrowsinthecalendar
- Move by day or week, previewing the range
- Enter
- Sets the start, then the end
- Escape
- Closes the calendar
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | The accessible name, drawn as the floating border label (or above, classic). Required. |
hint | unknown | — | One line of help under the control. |
error | unknown | — | Shown instead of the hint; marks the field invalid. |
icon | IconRef | — | Leading icon: it sweeps with the border. |
prefix | unknown | — | Text or an element inside the field before the value, such as $. |
suffix | unknown | — | Text or an element inside the field after the value, such as kg. |
variant | "floating" | "classic" | — | floating (default, or the app's warp({ fields })) · classic: the label above. |
appearance | "line" | — | line: only a bottom line under the value, no box or fill. The boxed field otherwise. |
size | "sm" | "md" | "lg" | 'md' | Control size. md by default. |
clearable | boolean | false | A clear button (shown once there is a value). |
counter | number | — | Maximum length, shown as a counter. |
id | string | — | The element id. One is made for you if you leave it out. |
disabled | boolean | false | Turns it off, and takes it out of the tab order. |
readonly | boolean | false | Shows the value and lets it be selected and copied, but not changed. |
required | boolean | false | The form will not submit without it. |
name | string | — | The name it posts under in a form. |
locale | string | — | The language and region used to format values. |
today | string | — | Pins "today" (server render, tests); the browser's clock otherwise. |
presentation | Presentation | — | auto opens a popover, or a sheet when the viewport is under 600px wide; popover and sheet force one. |
value | DateRange | State<DateRange> | — | The current value, when you own the state. |
defaultValue | DateRange | — | The starting value, when the component owns the state. |
open | boolean | State<boolean> | — | A state box that holds whether it is open. |
defaultOpen | boolean | false | Starts open, when the component keeps its own open state. |
presets | false | RangePreset[] | "default" | 'default' | A list of your own, "default", or false for none. |
preset | string | null | — | The id of the chosen preset, such as "Last 7 days". Null when the dates were picked by hand. |
min | `${number}-${number}-${number}` | — | The lowest allowed value. |
max | `${number}-${number}-${number}` | — | The highest allowed value. |
maxDays | number | — | The longest range allowed. |
isDateUnavailable | ((d: `${number}-${number}-${number}`) => boolean) | — | Return true for a day that can't start or end the range: it shows struck through. |
months | 2 | 1 | "auto" | 'auto' | Months shown side by side. auto follows the width. |
compare | RangeCompare | null | — | previous-period, previous-year or a range of your own. |
| Event | Type | Default | Description |
|---|---|---|---|
onInput | ((e: InputEvent & { currentTarget: HTMLInputElement & HTMLTextAreaElemen… | — | Called on each keystroke. |
onValueChange | ((v: DateRange, reason: ChangeReason) => void) | — | Called with each new value, and whether a pointer, a key or code made the change. |
onOpenChange | ((o: boolean) => void) | — | Called with true when it opens and false when it closes. |
onPresetChange | ((id: string | null) => void) | — | Called with the preset's id when one is picked, and null when the dates are changed by hand. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="range-panel"] [data-part="range-sep"]
range-sepfoot