Forms

DateRangeField

Controlled start/end date range picker with preset shortcuts and validation feedback for time-window filtering.

import { DateRangeField } from '@lostgradient/cinder/date-range-field';
formdatefilter
01

Overview

Controlled start/end date range picker with preset shortcuts and validation feedback, designed for time-window filtering on dashboards, event streams, and audit logs.

Overview

DateRangeField renders two text inputs backed by custom calendar/time popovers (start and end) along with preset shortcut buttons. It is fully controlled: the consumer owns the value and responds to onchange callbacks. It does not own routing, query-string synchronization, timezone conversion, or data fetching.

Values are ISO-8601 local strings. granularity="day" emits YYYY-MM-DD; time granularities use the custom time popover and emit values truncated to the selected precision.

Usage

svelte
<script lang="ts">
  import { DateRangeField } from '@lostgradient/cinder/date-range-field';
  import type { DateRangeValue } from '@lostgradient/cinder/date-range-field';

  let range: DateRangeValue = $state({ start: undefined, end: undefined });
</script>

<DateRangeField
  id="event-time-filter"
  label="Time window"
  bind:value={range}
  onchange={(next) => {
    range = next;
  }}
/>

With custom presets

svelte
<script lang="ts">
  import { DateRangeField } from '@lostgradient/cinder/date-range-field';
  import type { DateRangeDatePreset, DateRangeValue } from '@lostgradient/cinder/date-range-field';

  let range: DateRangeValue = $state({ start: undefined, end: undefined });

  function formatLocalDate(date: Date): string {
    const year = date.getFullYear();
    const month = String(date.getMonth() + 1).padStart(2, '0');
    const day = String(date.getDate()).padStart(2, '0');
    return `${year}-${month}-${day}`;
  }

  const presets: DateRangeDatePreset[] = [
    {
      id: 'today',
      label: 'Today',
      resolve: () => {
        const today = formatLocalDate(new Date());
        return { start: today, end: today };
      },
    },
    {
      id: 'this-month',
      label: 'This month',
      resolve: () => {
        const now = new Date();
        const start = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-01`;
        const end = formatLocalDate(now);
        return { start, end };
      },
    },
  ];
</script>

<DateRangeField id="billing-filter" label="Billing period" {presets} bind:value={range} />

With validation error

svelte
<script lang="ts">
  import { DateRangeField } from '@lostgradient/cinder/date-range-field';
  import type { DateRangeValue } from '@lostgradient/cinder/date-range-field';

  let range: DateRangeValue = $state({ start: undefined, end: undefined });

  let error = $derived(
    range.start && range.end && range.start > range.end
      ? 'Start date must be on or before the end date.'
      : undefined,
  );
</script>

<DateRangeField id="validated-filter" label="Date range" bind:value={range} {error} />

Props

plaintext
id           string               no        Unique identifier for label and ARIA wiring. Generated via `$props.id()` when omitted.
value        DateRangeValue       no        Current range. Bindable. Both fields start undefined.
label        string               no        Visible legend rendered above the inputs.
startLabel   string               no        Label for the start input. Default: "Start date".
endLabel     string               no        Label for the end input. Default: "End date".
granularity  DateRangeGranularity no        Precision for custom picker fields: day, hour, minute, or second. Default: day.
presets      DateRangeDatePreset[]  no      Custom preset buttons. Defaults to today, yesterday-today, last-7d.
hidePresets  boolean              no        When true, hides the preset row. Default: false.
description  string               no        Helper text below the field, wired via aria-describedby.
error        string               no        Validation error. Sets aria-invalid="true" on inputs.
disabled     boolean              no        Disables all inputs and preset buttons. Default: false.
class        string               no        Additional CSS classes on the root element.
onchange     (value) => void      no        Called when the range changes via preset or manual input.

Types

ts
type DateRangeValue = {
  start: string | undefined; // ISO-8601 local date or datetime string
  end: string | undefined; // ISO-8601 local date or datetime string
};

type DateRangeDatePreset = {
  id: string;
  label: string;
  resolve: () => DateRangeValue;
};

Accessibility

The component implements accessible form labelling throughout:

  • Each date input is associated with a <label> via for/id.
  • The optional legend (label prop) is a <p> associated with the group visually.
  • The preset button row carries role="group" with aria-label="Date range presets".
  • Each preset button carries aria-pressed to communicate current selection state to assistive technology.
  • The error region uses aria-live="polite" and is always in the DOM so screen readers reliably pick up the live region before text is injected.
  • When error is set, both inputs carry aria-invalid="true".
  • The root carries role="group" and aria-labelledby pointing to the legend element when a label prop is provided, associating the label with the start/end input group.
  • description and error elements are wired into each input via aria-describedby, so a screen reader user tabbing to an input hears the description and any active error.
  • Forced-colors (Windows High Contrast) mode: inputs and preset buttons receive a solid outline instead of box-shadow focus rings, which are ignored in that mode.

Scope limits

  • Timezone conversion is caller-owned. Emitted date-time values are local wall-clock strings without timezone offsets.
  • The component owns the custom date and date-time picker UI.
  • Start and end constrain each other through DatePicker's min/max contract: the calendar disables out-of-range dates and manual edits are validated and clamped. Consumers still own domain-specific validation and error messaging through the error prop.
Live preview
02

When to use

Use when
  • Filtering a list or dashboard by a start and end date (e.g. created between, updated between).
  • Offering common presets (last 7 days, last 24 hours) alongside a manual date range.
Avoid when
  • A single date is sufficient — use a plain date input instead.
  • Timezone conversion or a standalone time-of-day value is required — use time-field for the latter.
03

Examples

Basic date range

Start and end date inputs with built-in presets for today, yesterday & today, and last 7 days.

Custom presets

Consumer-supplied presets with labels suited to a billing or audit log context.

Disabled state

All inputs and preset buttons are disabled. Use when the date range cannot be edited in the current context.

Workflow list filter

Filter an operational workflow list by creation date with custom presets and validation feedback.

04

Props

Props for date-range-field
Name Type Default Required Bindable Description
id text Unique identifier used to generate accessible IDs for labels and error regions. Optional — a stable id is generated via $props.id() when omitted.
value { /** Start of the range as an ISO-8601 local string, or undefined when not set. */ start: string | undefined; /** End of the range as an ISO-8601 local string, or undefined when not set. */ end: string | undefined; } bind Current date range value. Bindable. Both fields start undefined when unset.
label text Visible legend rendered above the start/end inputs.
startLabel text Accessible label for the start input. Defaults to "Start date" for day granularity and "Start date and time" for datetime granularities.
endLabel text Accessible label for the end input. Defaults to "End date" for day granularity and "End date and time" for datetime granularities.
granularity 'day' | 'hour' | 'minute' | 'second' 'day' Date-time precision. Defaults to day precision.
presets DateRangeDatePreset[] Consumer-defined preset options shown above the date inputs. Each preset has a label and a resolve() function that returns a DateRangeValue. Defaults to today, yesterday-today, last-7d built-ins when omitted.
hidePresets boolean false When true, hides the preset buttons and shows only the date inputs.
description text Helper text displayed below the field; wired via aria-describedby.
error text Validation error message. When provided, marks both inputs as aria-invalid="true" and renders the message in a live region.
disabled boolean false Disables the entire field including presets and date inputs.
onchange (value: DateRangeValue) => void Called when the user changes the date range (preset or manual input).