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
Live · keyboard ready
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
Live · keyboard ready
billing-period-pick-a-start-and-an-end-e-g-to-filter-invoices-.tsxtsx
"use client";
import { useState } from "react";
import { DateRangePicker } from "@/components/manicat/date-range-picker/date-range-picker";
import type { DateRange } from "@/components/manicat/date-range-picker/date-range-picker";
export function DateRangePickerBillingExample() {
const [range, setRange] = useState<DateRange | null>(null);
return (
<div className="w-full max-w-sm">
<DateRangePicker label="Billing period" value={range} onChange={setRange} />
</div>
);
}
Keyboard interactions
| Keys | Action |
|---|---|
| Enter / Space | Opens the panel from the trigger; picks a start or end day in the grid. |
| Arrow keys | Move by day or week in the grid, extending the range preview once a start is picked. |
| Home / End | Jump to the start or end of the week. |
| PageUp / PageDown | Move by month; add Shift to move by year. |
| ArrowUp / ArrowDown | Move between presets in the rail; ArrowLeft and ArrowRight in the compact row. |
| Escape | Closes 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.