Problem Statement
Product forms often need to edit a short, ordered collection of consistent records: guest lists, traveler details, mailing-list entries, emergency contacts, tag options, and similar data. Each record usually has one or two simple fields, and users need to add, remove, edit, validate, and sometimes reorder rows without leaving the form.
Astryx currently provides the individual inputs and display-table primitives, but consumers must independently assemble the repeated-row layout, controlled collection updates, responsive behavior, focus policy, accessible names, validation placement, and reorder interactions. That creates substantial boilerplate and makes accessibility quality depend on every caller implementing the same behavior correctly.
Evidence of Demand
The pattern recurs across several product-neutral scenarios:
- Guest lists: name + email
- Traveler information: full name + passport country
- Tag options: color + label
- Mailing lists: display name + address
- Emergency contacts: name + phone
The intended design guidance is:
- Normally fewer than seven records
- Normally fewer than three fields per record
- A consistent field schema across rows
- Simple controls such as a single-line input, selector, checkbox, or switch
Those thresholds are guidance, not runtime restrictions. Larger consistent datasets should generally use Table; heterogeneous or complex records should generally use cards, tabs, or another progressive form pattern.
Public form libraries expose state primitives for repeated fields (for example Formik FieldArray, React Hook Form useFieldArray, and Ant Design Form.List), but a design-system component still needs to own the visual, interaction, and accessibility contracts.
Why Existing Components Don't Cover This
Table supplies semantic rows, columns, sizing, and overflow, but not row creation/removal, controlled item updates, collection validation, accessible reordering, or focus restoration.
FormLayout arranges independent controls but does not model a repeated ordered collection.
List is a display/navigation collection, not a form input.
Tokenizer represents scalar selections as tokens; it does not edit multiple fields within each record.
A recipe can demonstrate one instance, but every caller would still need to rebuild the same state transitions and accessibility behavior.
Proposed API: ListInput
API arbitration tested three mental models across guest, traveler, tag-option, dialog, narrow-container, and migration scenarios:
| Candidate |
Result |
Data-driven ListInput |
38/40 |
Compound EditableList |
33/40 |
Render-prop FieldArray |
16/40 |
ListInput was the most discoverable and concise. Typed column configuration stayed reusable, while the component retained ownership of semantics and interaction. EditableList added repeated compound-component ceremony. FieldArray encouraged callers to rebuild rows and native controls, weakening accessibility-by-default.
Provisional consumer API:
type TagOption = {
id: string;
color: string;
label: string;
};
const columns: ListInputColumn<TagOption>[] = [
{
key: 'color',
header: 'Color',
renderInput: ({item, updateItem, label, status, ...state}) => (
<Selector
label={label}
isLabelHidden
value={item.color}
onChange={color => updateItem({...item, color})}
status={status}
{...state}
/>
),
},
{
key: 'label',
header: 'Label',
renderInput: ({item, updateItem, label, status, ...state}) => (
<TextInput
label={label}
isLabelHidden
value={item.label}
onChange={labelValue => updateItem({...item, label: labelValue})}
status={status}
{...state}
/>
),
},
];
<ListInput
label="Tag options"
description="Choose a color and label for each option."
itemName="tag"
value={tags}
onChange={setTags}
getItemKey={tag => tag.id}
createItem={() => ({id: crypto.randomUUID(), color: '', label: ''})}
columns={columns}
isReorderable
status={listStatus}
getItemStatus={tag => itemStatuses[tag.id]}
getFieldStatus={(tag, columnKey) => fieldStatuses[tag.id]?.[columnKey]}
/>
Provisional types:
interface ListInputColumn<T> {
key: string;
header: string;
width?: ColumnWidth;
renderInput: (context: ListInputRenderContext<T>) => ReactNode;
renderValue?: (context: ListInputValueContext<T>) => ReactNode;
}
interface ListInputProps<T> {
label: string;
value: T[];
onChange: (next: T[], change: ListInputChange<T>) => void;
getItemKey: (item: T) => React.Key;
createItem: () => T;
columns: ListInputColumn<T>[];
itemName?: string;
description?: string;
status?: InputStatus;
getItemStatus?: (item: T, index: number) => InputStatus | undefined;
getFieldStatus?: (
item: T,
columnKey: string,
index: number,
) => InputStatus | undefined;
isReorderable?: boolean;
isReadOnly?: boolean;
isDisabled?: boolean;
isLoading?: boolean;
maxItems?: number;
}
ListInputChange<T> distinguishes add, update, remove, and reorder, including stable item key and relevant indexes/column key. Minimum-count validation remains caller-controlled through status; removal is not blocked merely because the resulting collection is invalid. maxItems is a physical constraint and disables Add at the limit.
Validation Contract
Validation is supported at three independent scopes:
- Field: Passed to the rendered input as an
InputStatus; the control owns its invalid border/icon/message and aria-invalid/aria-describedby wiring.
- Item: A full-row message associated with that stable row. It does not replace field errors.
- List: A full-width collection message rendered after the rows and Add action, associated with the collection.
All three may coexist. Errors stay attached to stable item keys after reorder. Validation timing remains application-owned: callers may compute statuses on change, blur, submit, or after an async response.
Accessibility Considerations
- Render semantic table structure with column headers associated to cells.
- Give the collection a programmatic name and description.
- Give every cell input a label combining column and item position.
- Use stable keys so values, errors, DOM identity, and focus survive reorder.
- Use real buttons with specific names for Add, Remove, and the reorder handle.
- Support pointer, touch, and keyboard reorder with the same commit path.
- During keyboard reorder, support start, move, commit, and cancel; announce position changes in a polite live region.
- Keep focus on the moved item after reorder.
- After Add, focus the first editable control in the new row.
- After Remove, focus the equivalent action in the next row, previous row, or Add button.
- Ensure drag never starts from a field or remove action.
- Use persistent field error text; validation cannot be conveyed only by color or a hover tooltip.
- Distinguish read-only, disabled, and loading behavior and ARIA.
Responsive Behavior
The component owns narrow-container behavior. Rows remain semantically tabular and horizontally scroll when their minimum column widths no longer fit. Long labels and validation messages wrap without requiring consumer wrapper CSS. The component does not switch to a different record format at runtime.
Performance Considerations
This component targets small collections, so v1 does not virtualize. Rendering cost is linear in items × columns and is intentionally bounded by the documented use case. Reorder may read the small set of row rectangles during an active pointer interaction; it should not continuously measure during ordinary rendering. Stable item keys prevent unnecessary remounts.
Implementation Plan
- Land an experimental implementation in
@astryxdesign/lab.
- Compose
Field, semantic Table elements, Astryx inputs, Button/IconButton, FieldStatus, and VisuallyHidden.
- Keep the reorder engine internal until a second consumer establishes a reusable hook contract.
- Add typed docs, Storybook scenarios, and colocated behavior/a11y tests.
- Test one-, two-, and three-column configurations; field/item/list errors; controlled external changes; add/remove; pointer/touch/keyboard reorder and cancellation; focus restoration; read-only/disabled/loading; empty/max states; narrow width; long localized content; and RTL.
- Harden in lab across themes and human design review before a separate core-graduation change.
Pre-submission Checklist
Problem Statement
Product forms often need to edit a short, ordered collection of consistent records: guest lists, traveler details, mailing-list entries, emergency contacts, tag options, and similar data. Each record usually has one or two simple fields, and users need to add, remove, edit, validate, and sometimes reorder rows without leaving the form.
Astryx currently provides the individual inputs and display-table primitives, but consumers must independently assemble the repeated-row layout, controlled collection updates, responsive behavior, focus policy, accessible names, validation placement, and reorder interactions. That creates substantial boilerplate and makes accessibility quality depend on every caller implementing the same behavior correctly.
Evidence of Demand
The pattern recurs across several product-neutral scenarios:
The intended design guidance is:
Those thresholds are guidance, not runtime restrictions. Larger consistent datasets should generally use
Table; heterogeneous or complex records should generally use cards, tabs, or another progressive form pattern.Public form libraries expose state primitives for repeated fields (for example Formik FieldArray, React Hook Form useFieldArray, and Ant Design Form.List), but a design-system component still needs to own the visual, interaction, and accessibility contracts.
Why Existing Components Don't Cover This
Tablesupplies semantic rows, columns, sizing, and overflow, but not row creation/removal, controlled item updates, collection validation, accessible reordering, or focus restoration.FormLayoutarranges independent controls but does not model a repeated ordered collection.Listis a display/navigation collection, not a form input.Tokenizerrepresents scalar selections as tokens; it does not edit multiple fields within each record.A recipe can demonstrate one instance, but every caller would still need to rebuild the same state transitions and accessibility behavior.
Proposed API:
ListInputAPI arbitration tested three mental models across guest, traveler, tag-option, dialog, narrow-container, and migration scenarios:
ListInputEditableListFieldArrayListInputwas the most discoverable and concise. Typed column configuration stayed reusable, while the component retained ownership of semantics and interaction.EditableListadded repeated compound-component ceremony.FieldArrayencouraged callers to rebuild rows and native controls, weakening accessibility-by-default.Provisional consumer API:
Provisional types:
ListInputChange<T>distinguishesadd,update,remove, andreorder, including stable item key and relevant indexes/column key. Minimum-count validation remains caller-controlled throughstatus; removal is not blocked merely because the resulting collection is invalid.maxItemsis a physical constraint and disables Add at the limit.Validation Contract
Validation is supported at three independent scopes:
InputStatus; the control owns its invalid border/icon/message andaria-invalid/aria-describedbywiring.All three may coexist. Errors stay attached to stable item keys after reorder. Validation timing remains application-owned: callers may compute statuses on change, blur, submit, or after an async response.
Accessibility Considerations
Responsive Behavior
The component owns narrow-container behavior. Rows remain semantically tabular and horizontally scroll when their minimum column widths no longer fit. Long labels and validation messages wrap without requiring consumer wrapper CSS. The component does not switch to a different record format at runtime.
Performance Considerations
This component targets small collections, so v1 does not virtualize. Rendering cost is linear in items × columns and is intentionally bounded by the documented use case. Reorder may read the small set of row rectangles during an active pointer interaction; it should not continuously measure during ordinary rendering. Stable item keys prevent unnecessary remounts.
Implementation Plan
@astryxdesign/lab.Field, semanticTableelements, Astryx inputs,Button/IconButton,FieldStatus, andVisuallyHidden.Pre-submission Checklist