Skip to content
txbrendan/ui

Period Picker

beta

One 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

brand

Sep 1 to Sep 29

pills only

Jul 1 to Sep 29

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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

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} />