Range Pair

Catches backwards, empty or wildly oversized date and number ranges, and repairs them the likely way (next year for Dec → Jan).

StopsBackwards date or price range

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/range-pair.json

The files land in your project. They're yours to edit.

Usage

range-pair-usage.tsx
import { RangePair } from "@/components/ui/range-pair"

<RangePair kind="date" label="Trip dates" maxSpan={60} defaultValue={{ from: "2026-12-28", to: "2026-01-03" }} />

What it catches

  • Reversed ends, with the likelier repair (next year for Dec → Jan)
  • Empty ranges when the end is exclusive
  • Oversized ranges from a year or extra-zero typo (maxSpan)
  • Plain swap as an alternative fix

Props

RangePair props
PropTypeDefault
kind

Numbers or dates.

RangeKind"number"
label

Legend for the pair.

string—
fromLabel

Labels of the two ends. Default "Min"/"Max" for numbers, "From"/"To" for dates.

stringkind === "date" ? "From" : "Min"
toLabel

Label for the end of the range.

stringkind === "date" ? "To" : "Max"
value

Controlled ends.

RangeValue—
defaultValue

Initial ends when uncontrolled.

RangeValue{ from: "", to: "" }
onValueChange

Called whenever either end changes.

(value: RangeValue) => void—
maxSpan

Spans above this (days for dates) get a "longer than expected" warning.

number—
exclusiveEnd

The end is not part of the range (hotel nights, `[from, to)`), so equal ends mean it's empty.

booleanfalse
locale

BCP 47 locale for numbers and dates.

string"en-IN"
delay

Pause in ms before checking while typing.

number700
reversedMessage

Shown when the ends are reversed.

stringkind === "date" ? "This ends before it starts." : "The ma…
emptyMessage

Shown when an exclusive range is empty.

string"Both ends are the same, so nothing falls in this range."
hugeMessage

Receives the formatted span, e.g. "1 year 31 days" or "49,500".

(span: string) => string(span) => `That's ${span}, longer than expected.`
swapLabel

Button text to swap the ends.

string"Swap"
fixLabel

Fix button text; receives which end changes and its formatted new value.

(side: "from" | "to", value: string) => string(side, v) => (kind === "date" ? `${side === "to" ? "End" …
spanText

Readback of the span; receives it formatted.

(span: string) => string(span) => (kind === "date" ? span : `Range of ${span}`)

* required. Also accepts the native props of <fieldset> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Both ends have visible labels and are described by a polite status region
  • Fix and swap are buttons; icons are aria-hidden
  • Focus returns to the From field after a fix

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Dates are YYYY-MM-DD in UTC; times of day aren't handled
  • maxSpan has to be set for size checks to mean anything

What it stops

Backwards date or price range

Also known as

date range · min max · price range · from to · filter · check in check out · range picker · start end

Dependencies

Pairs well with