Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Editable Table Form Block for React

A crew list as a table you edit in place: name, email and role cells, Enter to move down and to add a row, a remove button per member, cells outlined with their reason, and a Save that stays off until something changed.

Framework
UI library

Preview

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Overview

Let people fix a whole list in one place with this editable table form for React with Base UI. A compact header with an Add member button sits over a hairline frame with a plus mark on each corner. The first band is a real table of members, one row each: a name field, an email field and a role Native Select (Lead, Crew or Observer), with a Remove button named for the member. Tab walks across a row and down; Enter in a name or email cell moves to the same cell of the next row, and Enter in the last row adds a member and puts focus in its name. A cell that breaks a rule, a missing name, an email with no domain or the same email twice, is outlined once its row has been left or a save was tried, and the band under the table says which row and why. Removing the last member shows an Empty state with Add member inside the same held box, so the panel never changes height. The foot holds the Shine Button Save roster, the only colored control, with a polite status line and a Discard button. Both stay pressable but inert until something has changed, and say so. On a phone each row becomes a small stack. It's built from Input, Native Select, Empty, Button, Shine Button and Guides, with a CSS entrance only. For team lists, seat assignments, price rows and any form whose answer is a list of records.

FAQ

How do I change the columns and the starting list?

Edit SAVED for the members and ROLES for the role options. A column is an Input or NativeSelect inside MemberRow, wired through the Col type, so a new text column is one more cell, one more entry in Col, one more rule in problemsOf and one more header. The member count, the status line and the band under the table all read the same rows state.

Which keys does it handle?

Tab and Shift+Tab move between cells in reading order, as in any form. Enter in a name or email cell moves to the same cell one row down, and from the last row it adds a member and focuses the new name. Enter never submits the form from inside the table, so a stray press cannot save a half-finished list. The role cell keeps the browser's own select keys.

When does a cell show its error?

After you leave its row, or after a failed Save, never while you are still typing in it. The cell gets an outline and aria-invalid, and the band under the table lists up to three reasons with the row number. A failed Save also moves focus to the first cell with a problem and says how many need fixing. Fixing a cell removes its outline at once.

How does it look on a phone, and with a right-to-left page?

Below the medium breakpoint the table head hides and each row becomes a stack: name, email, then the role beside the remove button. In a right-to-left page the columns mirror, names are isolated so mixed scripts stay in order, and emails are kept left to right.

What stays local, and how do I connect it for real?

Everything: Save roster keeps the list in this tab's state and says "Saved 5 members in this demo. Nothing was sent." To make it real, post rows from the submit handler, check the same rules again on your server (a duplicate email is yours to enforce), and map a server error onto the row by its id so the cell is outlined the same way.

How do I connect it to react-hook-form or TanStack Form?

Treat the list as a field array and keep each row's id as its key. With react-hook-form, use useFieldArray for the rows, register each cell by members.${index}.name, and move the three rules in problemsOf into a resolver; keep keys for the Enter behavior.