DateRangePicker

DateRangePicker combines one trigger with Calendar range selection. Choose it when a start and end date describe one period; choose two DatePickers only when the dates have separate meaning or rules.

Examples

A complete travel period

The trigger displays the selected start and end date as one value. Selecting the second bound in the composed Calendar completes the range and closes the picker.

8/11/2026 to 8/15/2026

Translated range text

labels.rangeSeparator and labels.incompleteRange control the text between a start and end date and the marker shown while only the start exists. labels.rangeAnnouncement(start, end) controls the accessible description for a complete range, announced after the field label; include both formatted dates in its result.

When to use

Use DateRangePicker when one person needs to choose a contiguous local-date period, such as travel, availability or a reporting window.

Reach for something else when:

  • Only one date matters — use DatePicker.
  • The two dates have distinct labels, validation or workflow steps — use separate DatePickers.
  • The date grid belongs permanently in the page — use Calendar directly.

Accessibility

  • The trigger is a native button associated with label through htmlFor. On desktop it is the Popover trigger, with its non-modal dialog semantics, Escape dismissal and focus return.
  • The composed Calendar runs in range mode. It retains roving day focus and keyboard navigation; the first selection starts a range, the next finishes it, and a completed range closes the picker.
  • The trigger's visible value is a single formatted string: start date, labels.rangeSeparator, then the end date or labels.incompleteRange. The field label stays the accessible name. With a label, a complete range is also described as “X to Y” by default through a visually hidden element linked by aria-describedby. Customize it with labels.rangeAnnouncement(start, end) in React or the rangeAnnouncement Alpine option. Empty and incomplete ranges have no description.
  • error replaces hint; disabled disables the trigger.
  • At viewports of 640px or less, the Popover path becomes a BottomSheet containing Calendar. Its title and close name come from label/labels.sheetTitle and labels.close. It is prose only in these docs because the stage cannot resize its viewport.

API and code

NameTypeRequiredDescription
labelstring—Label rendered above the picker trigger.
hintstring—Helper text rendered below the picker. Replaced by `error` when set.
errorstring—Error message that enables error styling and replaces `hint`.
valueDateRange | null—Controlled local-date range, or `null` with no range selected.
defaultValueDateRange | { start: Date | string; end: Date | string; }—Initial local-date range. ISO `YYYY-MM-DD` strings are accepted for convenience.
onChange(range: DateRange) => void—Called after the user selects either bound of the local-date range.
placeholderstring—Trigger text when no range is selected. Defaults to `labels.placeholder`.
minDate | string—Inclusive lower selection limit as a local date or ISO `YYYY-MM-DD` string.
maxDate | string—Inclusive upper selection limit as a local date or ISO `YYYY-MM-DD` string.
localestring—BCP 47 locale used to format selected dates and compose Calendar. Default: `"en-US"`.
labelsDateRangePickerLabels—Translatable visible and accessible labels, merged over the English defaults.
disabledboolean—Disables the picker trigger.

x-data="lyraDateRangePicker({ … })"

OptionTypeRequiredDescription
defaultValue{ start?: string | null; end?: string | null; } | null—Initial ISO `YYYY-MM-DD` bounds.
localestring—BCP 47 locale used for the trigger's selected-date text. Default: `"en-US"`.
placeholderstring—Trigger text when no range has started. Default: `"Select period"`.
rangeSeparatorstring—Text between formatted range bounds. Default: `" – "`.
incompleteRangestring—Text after the separator while the range has no end. Default: `"…"`.
rangeAnnouncement(start: string, end: string) => string—Accessible description of a complete range, referenced by `aria-describedby`. Includes both formatted dates. Default: `"X to Y"`.

Without React, compose one trigger with Popover and Calendar classes, then implement range state, focus movement and the mobile sheet alternative yourself:

html
<div class="lyra-field">
  <label class="lyra-label" for="travel-dates">Travel dates</label>
  <span class="lyra-datepicker lyra-popover-anchor">
    <button
      class="lyra-input lyra-datepicker__btn"
      id="travel-dates"
      type="button"
      aria-haspopup="dialog"
      aria-expanded="true"
      aria-describedby="range-announcement"
    >
      <span>08/11/2026 – 08/15/2026</span>
    </button>
    <span id="range-announcement" class="lyra-visually-hidden">08/11/2026 to 08/15/2026</span>
    <div
      class="lyra-popover lyra-popover--bottom lyra-popover--align-start"
      role="dialog"
      aria-label="Date range picker"
    >
      <div class="lyra-cal">
        <button class="lyra-cal__day lyra-cal__day--selected" type="button" aria-pressed="true">
          11
        </button>
        <button class="lyra-cal__day lyra-cal__day--in-range" type="button">12</button>
        <button class="lyra-cal__day lyra-cal__day--selected" type="button" aria-pressed="true">
          15
        </button>
      </div>
    </div>
  </span>
</div>

<lyra:date-range-picker> Generated from lyra-ds/blade v0.10.0.

The behavior comes from lyraDateRangePicker() — install @lyra-ds/alpine and see the HTML + Alpine tab.

PropDefaultRequiredExample values
labelnull—0 Period
hintnull——
errornull——
defaultValuenull——
placeholdernull——
minnull——
maxnull——
locale'en-US'——
labels[]——
disabledfalse——
namenull——
blade
<lyra:date-range-picker
    name="reporting_period"
    label="Reporting period"
    :default-value="['start' => '2026-03-01', 'end' => '2026-03-31']"
    min="2026-01-01"
    max="2026-12-31"
    locale="en-US"
/>