Date range picker

A range picker that grows from its trigger into two months with presets and a stretching range highlight.

inputsnew
Preview
Date range picker preview

When to use

  • Analytics and reporting filters that need presets like Last 7 days or This quarter.
  • Booking or leave requests where a start and end day are picked together.
  • Toolbars where the picker must stay compact until opened.

When not to use

  • Use date-picker for a single date.
  • Use calendar when the month should stay visible on the page.
  • Use time-picker when the user chooses a time of day rather than days.

shadcn CLI

Also installs (1)

  • manicat-foundation
Manual install(+)
registry.jsonjson
{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "date-range-picker",
  "type": "registry:ui",
  "title": "Date range picker",
  "description": "A range picker that grows from its trigger into two months with presets and a stretching range highlight.",
  "categories": [
    "inputs"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://manicat.viraza.net/r/manicat-foundation.json"
  ],
  "files": [
    {
      "path": "registry/components/date-range-picker/date-range-picker.tsx",
      "type": "registry:component",
      "target": "@components/manicat/date-range-picker/date-range-picker.tsx"
    },
    {
      "path": "registry/components/date-range-picker/date-range-picker.module.css",
      "type": "registry:component",
      "target": "@components/manicat/date-range-picker/date-range-picker.module.css"
    }
  ],
  "docs": "Docs and live preview: https://uiarc.dev/components/date-range-picker",
  "meta": {
    "tier": "free",
    "kind": "component",
    "docs": "https://manicat.viraza.net/components/date-range-picker",
    "markdown": "https://uiarc.dev/components/date-range-picker/markdown",
    "tags": [
      "inputs",
      "new"
    ]
  }
}

Usage

Install the component, then copy this into your app. Change the content and the props to fit your screen.

example.tsxtsx
"use client";

import { useState } from "react";
import type { DateRange } from "@/components/manicat/date-range-picker/date-range-picker";
import { DateRangePicker } from "@/components/manicat/date-range-picker/date-range-picker";

export function DateRangePickerExample() {
  const [range, setRange] = useState<DateRange | null>(null);

  return <DateRangePicker label="Billing period" value={range} onChange={setRange} />;
}

Examples

Real screens rather than isolated props. Switch to Code to copy a whole example into your project.

Billing period Pick a start and an end, e.g. to filter invoices.

Billing period Pick a start and an end, e.g. to filter invoices. preview

Keyboard interactions

KeysAction
Enter / SpaceOpens the panel from the trigger; picks a start or end day in the grid.
Arrow keysMove by day or week in the grid, extending the range preview once a start is picked.
Home / EndJump to the start or end of the week.
PageUp / PageDownMove by month; add Shift to move by year.
ArrowUp / ArrowDownMove between presets in the rail; ArrowLeft and ArrowRight in the compact row.
EscapeCloses without applying and returns focus to the trigger.

Accessibility

  • The trigger has aria-haspopup="dialog", aria-expanded, and a label that includes the current range.
  • Each month is a role="grid" labelled by its title, with columnheaders and gridcells using aria-selected for days in range and aria-current="date" for today.
  • Days have full date labels and a roving tabindex; months that are leaving turn inert so focus only finds the current set.
  • A polite live region announces the start day, then the range and its length.

Motion

  • The trigger surface grows into the panel on a physical spring and shrinks back on Apply while the formatted label flies into the trigger with a shared layoutId.
  • The range highlight is one bar per week that stretches as you hover, and the two ends glide between days.
  • Months slide in the direction of travel, and label words roll up or down with time's direction.
  • Reduced motion jumps the surface size, drops the shared layout and slide, and uses short opacity fades.

Responsive behavior

  • months="auto" shows two months beside a vertical preset rail when the boundary is at least 712px wide, otherwise one month under a scrolling preset row.
  • In compact mode the panel width is min(352px, available) and day cells size between 32 and 42px to fit.
  • The panel shifts horizontally to stay 8px inside the boundary, and recomputes on window resize.

Performance

  • ResizeObservers on the trigger and panel feed the size springs; updates are batched through a microtask.
  • Each week's highlight and each end is a motion value animation, so hovering does not re-render the grid beyond the range change.
  • Intl.DateTimeFormat instances are memoized per locale.

Notes for AI

  • Use when a start and end date are chosen together: report filters, analytics periods, bookings.
  • Selection is draft until Apply; onChange fires only on Apply. Cancel, Escape, or an outside click discards the draft.
  • Pass custom presets as { label, range: today => ({ start, end }) }; today is the viewer's local date.
  • The trigger is disabled until hydration because today is unknown on the server.
  • For a single date use date-picker; for an always-visible month use calendar.