Repeat Rows

Reduces lost, doubled and blank entries in add-another forms with undoable removes, duplicate flags and min/max limits.

StopsDuplicate line items in add-another form

Installation

pnpm dlx shadcn@latest add https://bollard-ui.vercel.app/r/repeat-rows.json

The files land in your project. They're yours to edit.

Usage

repeat-rows-usage.tsx
import { cleanRows, RepeatRows, type RepeatField, type RepeatRow } from "@/components/ui/repeat-rows"

<RepeatRows label="Guests" itemLabel="Guest" fields={fields} value={guests} onValueChange={setGuests} uniqueBy={["email"]} min={1} max={5} />

What it catches

  • Duplicate rows, compared case- and space-insensitively on uniqueBy keys
  • Blank rows, with a one-tap clean-up
  • Accidental removes, with Undo in place
  • Enter in the last row adding a row instead of submitting
  • Min and max row counts

Props

RepeatRows props
PropTypeDefault
label* (required)

Legend for the group.

string—
fields* (required)

The columns in each row.

RepeatField[]—
value

Controlled rows.

RepeatRow[]—
defaultValue

Initial rows when uncontrolled.

RepeatRow[]—
onValueChange

Called with every row, blanks included. Use `cleanRows` before saving.

(rows: RepeatRow[]) => void—
min

Fewest rows; remove is disabled at this count.

number1
max

Most rows; add is disabled at this count.

numberInfinity
uniqueBy

Fields that make two rows the same thing, e.g. `["email"]`.

string[]—
itemLabel

What one row is, e.g. "Guest".

string"Item"
undoMs

How long Undo stays after a remove, in ms.

number5000
addLabel

Label of the add-row button.

string"Add {item}"
undoLabel

Label of the Undo button after a remove.

string"Undo"
duplicateText

Templates: `{item}`, `{n}`, `{max}` and `{count}` are filled in.

string"Same as {item} {n}."
removeDuplicateLabel

Label of the one-tap remove on a duplicate row.

string"Remove this one"
emptyText

Shown when blank rows would be submitted.

string"{count} blank {rows} will be skipped."
cleanLabel

Label of the button that removes blank rows.

string"Remove blank rows"
maxText

Shown when the maximum number of rows is reached.

string"That's the limit of {max}."

* required. Also accepts the native props of <fieldset> (className, aria-*, data-*…), passed through to it.

Accessibility

  • Each row is a group labelled "Item n of m"; inputs carry per-row aria-labels
  • Enter moves to the next row or adds one; focus moves to the new or next logical field after add/remove
  • Remove buttons have aria-labels and disable at the minimum
  • Duplicate and blank notices render in polite live regions

Only what the source does. Test with your own screen reader and content before shipping.

Limitations

  • Duplicate check compares only the keys you list in uniqueBy
  • Undo is time-limited (undoMs) and in-memory
  • Validation of field contents is up to you

What it stops

Duplicate line items in add-another form

Also known as

add another · field array · line items · useFieldArray · guests · invoice items · dynamic rows · react hook form

Field type

Dependencies

Pairs well with