Skip to content

Documentation

neelam-ui

Search documentation

Getting Started
Forms
Overlays
Navigation
Data Display
Layout
AI & Chat
Blocks
GitHub repository

Date Range Picker

The time-period filter a dashboard is read through — a trigger showing the current range, relative presets, and two labelled calendars for a custom span.

No range selected

Usage#

import { DateRangePicker, type DateRange } from "neelam-ui";
 
const [range, setRange] = useState<DateRange | undefined>();
 
<DateRangePicker
  value={range}
  onValueChange={setRange}
  disabled={(date) => date > new Date()}
/>

DateRange is an inclusive span: from and to may be the same day.

Assembled from what already exists#

Almost none of this is new machinery:

  • Popover for the panel — and with it Escape, outside-click dismissal, and top-layer rendering, none of it reimplemented.
  • Two Calendars for the custom range.
  • RadioGroup for the presets.

That last one matters most. The presets are mutually exclusive options, which is exactly a radio group — so making them real <input type="radio">s means arrow-key navigation, mutual exclusivity, and the "3 of 5" announcement all come from the browser rather than from a hand-rolled listbox with its own roving focus.

Two calendars, not one#

A single grid with range-painting has to express "click once for the start, again for the end" through hover preview alone — invisible to keyboard and touch users, and unannounced to assistive tech.

Two labelled start/end grids make the same selection unambiguous on every input mode, and let Calendar stay the single-date component it already is. The end grid disables everything before the start, and vice versa, so an inverted range cannot be produced at all.

Presets#

defaultDateRangePresets covers what a dashboard filter usually wants. Replace them with presets, or pass [] for a custom-range-only picker.

A preset stores a function, not a range, so "Last 7 days" is recomputed at selection time:

{ label: "Last 7 days", getValue: () => ({ from: addDays(new Date(), -6), to: new Date() }) }

Why a function, not a value

A stored range would silently mean last week in a tab left open overnight. Recomputing on selection is what keeps a relative preset relative.

Sizing the trigger#

Use the size prop — "sm", "md" (default), or "lg" — rather than a className height.

A className height will not replace the default

The trigger already carries buttonVariants({ size: "md" }). A plain className="h-8" does not replace those classes, it sits alongside the existing h-10 — two conflicting utilities on one element, with whichever Tailwind emits later in its generated stylesheet silently winning.

Keyboard#

Keyboard shortcuts
KeyBehaviour
EnterSpaceOpens the panel from the trigger.
Moves between presets — native radio-group behaviour.
TabMoves from the presets into the start grid, then the end grid.
Moves within a calendar grid, once focus is inside one.
EscapeCloses the panel and returns focus to the trigger.

Everything is reachable without a pointer — which is the point of choosing a radio group and two grids over a hover-driven single grid.

Accessibility#

  • The trigger's accessible name comes from label (default "Date range"), and the formatted range is its visible text.
  • Presets are real radio inputs in a labelled group, so their count and position are announced by the platform.
  • Each calendar is labelled as the start or end grid, so it is never ambiguous which endpoint is being set.
  • Invalid ranges are prevented structurally by disabling dates, rather than reported as an error after the fact — the better of the two, since there is no error state to announce or recover from.
  • disabled blocks dates in both grids, so a rule like "no future dates" is enforced on either endpoint.

API reference#

DateRangePicker#

Props for DateRangePicker
PropTypeDefault
classNamestring
defaultValue

Initial range when uncontrolled.

DateRange
disabled

Marks dates unselectable in the custom-range calendars, e.g. `(date) => date > new Date()` to block the future.

((date: Date) => boolean)
formatValue

Overrides how the selected range is written on the trigger.

((range: DateRange) => string)
label

The trigger's accessible name. Defaults to `"Date range"`.

stringDate range
onValueChange((range: DateRange) => void)
placeholder

Rendered on the trigger when nothing is selected yet.

ReactNodePick a date range
presets

Replaces the preset rows. Pass `[]` for a custom-range-only picker.

DateRangePreset[][ { label: "Last 7 days", getRange: () => ({ from: addDays(stripTime(new Date()), -6), to: stripTime(new Date()) }) }, { label: "Last 30 days", getRange: () => ({ from: addDays(stripTime(new Date()), -29), to: stripTime(new Date()) }) }, { label: "Last 90 days", getRange: () => ({ from: addDays(stripTime(new Date()), -89), to: stripTime(new Date()) }) }, { label: "Month to date", getRange: () => { const today = stripTime(new Date()); return { from: new Date(today.getFullYear(), today.getMonth(), 1), to: today }; }, }, { label: "Year to date", getRange: () => { const today = stripTime(new Date()); return { from: new Date(today.getFullYear(), 0, 1), to: today }; }, }, ]
size

The trigger button's size, same vocabulary as `Button`'s own (minus `"icon"`, which doesn't fit a trigger that always renders a label). Defaults to `"md"`, matching `Button`'s own default. Pick this instead of trying to shrink the trigger via `className`: the trigger already carries `buttonVariants({ size: "md" })`, so a plain `className="h-8 ..."` override doesn't replace those classes, it just adds `h-8` alongside the existing `h-10` — two conflicting utility classes on the same element, with whichever Tailwind happens to emit later in its generated stylesheet silently winning.

enummd
value

The selected range.

DateRange

DateRange#

Props for DateRange
PropTypeDefault
fromrequiredDate
torequiredDate

DateRangePreset#

Props for DateRangePreset
PropTypeDefault
getRangerequired

Computed on selection rather than stored, so "Last 7 days" stays relative to today across a long-lived session.

() => DateRange
labelrequired

Shown in the preset list, and matched against to decide which row is selected.

string