Period Picker
betaOne period for a page of charts: period pills, the days they cover said once, Custom dates, and a switch to compare with the period before.
Preview
report
Jul 1 to Sep 29
Tasks done on time
82%
{"period":"quarter"}
custom
Sep 14 to Sep 27
error
The end is before the start.
brand
Sep 1 to Sep 29
pills only
Jul 1 to Sep 29
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/period-picker
Use it for
- The period for a whole page of charts and numbers, like a monthly sales report.
- Pass periods, each with an id and a label. Give each a range, like 'Jul 1 to Sep 29', so the days are said once.
- Give each period a previous, like 'last quarter', so the switch reads 'Compare with last quarter'.
- Changes apply at once. Update the charts in onValueChange, with no Apply button.
- Pass today, as an ISO day, so Custom starts on the four weeks up to it and no date can pass it.
Avoid it for
- A range for one chart. Give that chart its own tabs.
- Picking one day, like a due date. Use Calendar.
- A form people submit later. Use date fields in the form.
Props
- periods
Required. The period pills, in order. Custom comes after them.
{ id: string; label: string; range?: string; previous?: string }[]
- value
The picked period, when controlled. from and to are ISO days, set only when period is 'custom'.
{ period: string; compare?: boolean; from?: string; to?: string }
- defaultValue
The first value when uncontrolled.
PeriodValue · default the first period
- onValueChange
Called at once on each change. While a custom end is before its start, it waits.
(value: PeriodValue) => void
- custom
Offer Custom, which opens From and To date fields.
boolean · default true
- compare
Offer the switch to compare with the period before.
boolean · default true
- today
Today as an ISO day. Custom starts on the four weeks up to it, and no date can pass it. One that is not a whole day falls back to the browser's today.
string · default today
- tone
The color of the picked pill.
"neutral" | "brand" · default "neutral"
- aria-label
Names the pills for screen readers.
string · default "Period"
- locale
The locale for the custom days, like 'Sep 2 to Sep 29'.
string · default "en-US"
- className
Extra classes for the root. Later classes win.
string
| Prop | Type | Default | What it does |
|---|---|---|---|
| periods | { id: string; label: string; range?: string; previous?: string }[] | - | Required. The period pills, in order. Custom comes after them. |
| value | { period: string; compare?: boolean; from?: string; to?: string } | - | The picked period, when controlled. from and to are ISO days, set only when period is 'custom'. |
| defaultValue | PeriodValue | the first period | The first value when uncontrolled. |
| onValueChange | (value: PeriodValue) => void | - | Called at once on each change. While a custom end is before its start, it waits. |
| custom | boolean | true | Offer Custom, which opens From and To date fields. |
| compare | boolean | true | Offer the switch to compare with the period before. |
| today | string | today | Today as an ISO day. Custom starts on the four weeks up to it, and no date can pass it. One that is not a whole day falls back to the browser's today. |
| tone | "neutral" | "brand" | "neutral" | The color of the picked pill. |
| aria-label | string | "Period" | Names the pills for screen readers. |
| locale | string | "en-US" | The locale for the custom days, like 'Sep 2 to Sep 29'. |
| className | string | - | Extra classes for the root. Later classes win. |
Accessibility
- The pills are Tabs, one Tab stop, moved with the arrow keys. Enter or Space picks one, so passing a pill does not reload the charts. The list is named 'Period' and described by the days it covers.
- The days are a polite live region, so a new period's days are read once.
- The compare switch is named by its words, like 'Compare with last quarter'.
- From and To are native date fields with visible labels, so each browser gives its own keyboard and screen reader support.
- An end before the start marks To as invalid and says so in a role="alert" line under the fields.
Examples
For a page of charts
const PERIODS = [
{ id: "quarter", label: "This quarter", range: "Jul 1 to Sep 29", previous: "last quarter" },
{ id: "month", label: "This month", range: "Sep 1 to Sep 29", previous: "August" },
];
const [period, setPeriod] = useState({ period: "quarter" });
<PeriodPicker periods={PERIODS} value={period} onValueChange={setPeriod} today="2026-09-29" />Pills only
<PeriodPicker periods={PERIODS} custom={false} compare={false} />