Skip to content
txbrendan/ui

Repeat Field

beta

How a session repeats, one question at a time: once, every week, or every 2 weeks, then the days, then until when. Under it, the series in words with its count, and any sessions that clash with a closure, with a way to skip them.

Preview

season

Repeats
On
Until

Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions

Room 2 is closed for repainting

The office is closed over Christmas

{"every":"week","days":[1,3],"until":"season"}

states

Once

Once, Tue 6 Oct, 10:00 to 11:00

No season end
On
Until

Every 2 weeks on Mon and Fri, 18:00 to 19:00. Choose when it ends.

Disabled
On
Until

Every Tue, until Sat 15 May 2027 · 32 sessions

size

Repeats, sm
On
Until

Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions

Room 2 is closed for repainting

Repeats, md
On
Until

Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions

Room 2 is closed for repainting

Repeats, lg
On
Until

Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions

Room 2 is closed for repainting

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/repeat-field

Use it for

  • Setting up a class, a team meeting, or any session that repeats through a season.
  • Pass start, the first session, and time, which the series says back, like "19:00 to 20:30".
  • Pass seasonEnd to offer "End of season". Without it, the field asks for a last day.
  • Pass closed with each closure and why, like a room being repainted. Sessions that fall in one say so, and Skip them leaves them out.
  • Skip them goes on ⌘Z for 8 seconds, and a Toaster on the page shows it with Undo.
  • Every 2 weeks counts from the start's own week.

Avoid it for

  • A single date. Use a date picker.
  • Rules a calendar app needs, like the last Friday of each month. This covers the weekly kinds a class or a team uses.
  • Choosing days with no session to repeat. Use choice chips.

Props

  • start

    The first session, "2026-10-06". Required.

    string

  • time

    Said back in the series, like "19:00 to 20:30".

    string

  • seasonEnd

    The season's last day, "2027-05-15". Offers "End of season", picked by default.

    string

  • closed

    Days the session can't happen, as inclusive ranges with why, like "Room 2 is closed for repainting".

    { from: string; to: string; reason: string }[]

  • value

    The answers. days are weekdays, 0 is Monday. until is a day, "season", or null to ask. skip lists days left out. Use with onValueChange to control it.

    { every: "none" | "week" | "two-weeks"; days?: number[]; until?: string | null; skip?: string[] }

  • defaultValue

    The answers at first render, when you do not control it. Default once.

    RepeatValue

  • onValueChange

    Called with the answers on each change, skips too.

    (value: RepeatValue) => void

  • label

    Names the first question.

    ReactNode · default "Repeats"

  • size

    Text, card, and chip size.

    "sm" | "md" | "lg" · default "md"

  • disabled

    Turns every question off.

    boolean · default false

  • locale

    A language tag for days and dates, which reads "Sat 15 May".

    string · default "en-GB"

  • className

    Classes for the outer box.

    string

Accessibility

  • How often is a radio group named by the label. The arrow keys move and choose.
  • The days are a group of checkboxes named "On". Until is a radio group, and its date opens a calendar.
  • Each clash says its status in words, "3 sessions clash", then why. Its dates show on hover, focus, or tap.
  • Skip them moves focus to the series it changed, and a polite status says how many were skipped.
  • ⌘Z puts them back, and the page says so once.

Examples

Weekly to the season's end, with a closure

<RepeatField
  start="2026-10-06"
  time="19:00 to 20:30"
  seasonEnd="2027-05-15"
  closed={[{ from: "2026-10-13", to: "2026-10-20", reason: "Room 2 is closed for repainting" }]}
  defaultValue={{ every: "week", days: [1, 3], until: "season" }}
/>

Once, until it repeats

<RepeatField start="2026-10-06" time="10:00 to 11:00" />