Repeat Field
betaHow 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
Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions
3 sessions clashRoom 2 is closed for repainting
4 sessions clashThe office is closed over Christmas
{"every":"week","days":[1,3],"until":"season"}
states
Once, Tue 6 Oct, 10:00 to 11:00
Every 2 weeks on Mon and Fri, 18:00 to 19:00. Choose when it ends.
Every Tue, until Sat 15 May 2027 · 32 sessions
size
Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions
3 sessions clashRoom 2 is closed for repainting
Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions
3 sessions clashRoom 2 is closed for repainting
Every Tue and Thu, 19:00 to 20:30, until Sat 15 May 2027 · 64 sessions
3 sessions clashRoom 2 is closed for repainting
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/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
| Prop | Type | Default | What it does |
|---|---|---|---|
| start | string | - | The first session, "2026-10-06". Required. |
| time | string | - | Said back in the series, like "19:00 to 20:30". |
| seasonEnd | string | - | The season's last day, "2027-05-15". Offers "End of season", picked by default. |
| closed | { from: string; to: string; reason: string }[] | - | Days the session can't happen, as inclusive ranges with why, like "Room 2 is closed for repainting". |
| value | { every: "none" | "week" | "two-weeks"; days?: number[]; until?: string | null; skip?: string[] } | - | 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. |
| defaultValue | RepeatValue | - | The answers at first render, when you do not control it. Default once. |
| onValueChange | (value: RepeatValue) => void | - | Called with the answers on each change, skips too. |
| label | ReactNode | "Repeats" | Names the first question. |
| size | "sm" | "md" | "lg" | "md" | Text, card, and chip size. |
| disabled | boolean | false | Turns every question off. |
| locale | string | "en-GB" | A language tag for days and dates, which reads "Sat 15 May". |
| className | string | - | Classes for the outer box. |
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" />