Field List
A repeating set of form rows the user can add to, remove from and reorder.
<arc-field-list>Overview
FieldList is for a form field that repeats: the options of a poll, the levels of a scale, a list of email addresses. Each row is an <arc-field-row> holding whatever inputs it needs, with a handle to reorder it and a button to remove it, and the list adds an Add button below.
Your application owns the rows. Render them from your own array. The list never adds, removes or moves an element itself: it fires arc-add, arc-remove (with the index) and arc-move (with from and to), and you update your array. Keeping a single copy of the data means nothing can drift out of step. Everything around the data is the list's job: min and max, reordering by keyboard (the arrow keys on a row's handle) and by dragging, putting focus back where the user expects after each change, and a spoken confirmation of every change for screen reader users.
The rows are plain light DOM, so their inputs submit with the surrounding form as they are named.
Guidelines
When to use
- Give the list a `label` and each row a `label` ("Option 2") so controls read as "Remove Option 2"
- Update your array on arc-add, arc-remove and arc-move; the rows follow when you re-render
- Set `min` to the fewest rows the form can accept, so the last required row cannot be removed
When not to use
- Do not add, remove or reorder rows in the DOM behind the list's back while a change it asked for is pending
- Do not use it for a fixed number of fields; plain inputs are clearer
Features
- Add, remove and reorder rows of any inputs
- Reorder with the arrow keys on a row handle, or by dragging it
- `min` and `max`, which disable Remove and Add at the limits
- Focus follows the change: into a new row, onto a moved row, onto the row that took a removed one's place
- Each change announced in a polite live region; each control named after its row
- Your array stays the only copy: the list asks, your code changes it
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available. Use the Web Component directly or a framework wrapper.
<arc-field-list label="Options" add-label="Add option" min="2" max="6">
<arc-field-row label="Option 1"><arc-input label="Option 1" name="option" value="Yes"></arc-input></arc-field-row>
<arc-field-row label="Option 2"><arc-input label="Option 2" name="option" value="No"></arc-input></arc-field-row>
</arc-field-list>
<script>
// The list asks; your code changes the rows.
const list = document.querySelector('arc-field-list');
list.addEventListener('arc-add', () => { /* append a row to your data and re-render */ });
list.addEventListener('arc-remove', (e) => { /* remove row e.detail.index */ });
list.addEventListener('arc-move', (e) => { /* move row e.detail.from to e.detail.to */ });
</script>import { FieldList, FieldRow, Input } from '@arclux/arc-ui-react';
import { useState } from 'react';
export default function Example() {
const [options, setOptions] = useState(['Yes', 'No']);
const move = (from, to) => setOptions((o) => { const n = [...o]; n.splice(to, 0, n.splice(from, 1)[0]); return n; });
return (
<FieldList label="Options" addLabel="Add option" min={2} max={6}
onArcAdd={() => setOptions((o) => [...o, ''])}
onArcRemove={(e) => setOptions((o) => o.filter((_, i) => i !== e.detail.index))}
onArcMove={(e) => move(e.detail.from, e.detail.to)}>
{options.map((value, i) => (
<FieldRow key={i} label={`Option ${i + 1}`}>
<Input label={`Option ${i + 1}`} value={value} name="option" />
</FieldRow>
))}
</FieldList>
);
}<script setup>
import { ref } from 'vue';
import { FieldList, FieldRow, Input } from '@arclux/arc-ui-vue';
const options = ref(['Yes', 'No']);
function move(from, to) {
const [row] = options.value.splice(from, 1);
options.value.splice(to, 0, row);
}
</script>
<template>
<FieldList label="Options" add-label="Add option" :min="2" :max="6"
@arc-add="options.push('')"
@arc-remove="(e) => options.splice(e.detail.index, 1)"
@arc-move="(e) => move(e.detail.from, e.detail.to)">
<FieldRow v-for="(value, i) in options" :key="i" :label="`Option ${i + 1}`">
<Input :label="`Option ${i + 1}`" :value="value" name="option" />
</FieldRow>
</FieldList>
</template><script>
import { FieldList, FieldRow, Input } from '@arclux/arc-ui-svelte';
let options = ['Yes', 'No'];
function move(from, to) {
const next = [...options];
next.splice(to, 0, next.splice(from, 1)[0]);
options = next;
}
</script>
<FieldList label="Options" addLabel="Add option" min={2} max={6}
on:arc-add={() => (options = [...options, ''])}
on:arc-remove={(e) => (options = options.filter((_, i) => i !== e.detail.index))}
on:arc-move={(e) => move(e.detail.from, e.detail.to)}>
{#each options as value, i}
<FieldRow label={`Option ${i + 1}`}>
<Input label={`Option ${i + 1}`} {value} name="option" />
</FieldRow>
{/each}
</FieldList>import { Component } from '@angular/core';
import { FieldList, FieldRow, Input } from '@arclux/arc-ui-angular';
@Component({
imports: [FieldList, FieldRow, Input],
template: `
<arc-field-list label="Options" add-label="Add option" [min]="2" [max]="6"
(arc-add)="options.push('')"
(arc-remove)="options.splice($event.detail.index, 1)"
(arc-move)="move($event.detail.from, $event.detail.to)">
@for (value of options; track $index) {
<arc-field-row [label]="'Option ' + ($index + 1)">
<arc-input [label]="'Option ' + ($index + 1)" [value]="value" name="option"></arc-input>
</arc-field-row>
}
</arc-field-list>
`,
})
export class MyComponent {
options = ['Yes', 'No'];
move(from: number, to: number) {
const [row] = this.options.splice(from, 1);
this.options.splice(to, 0, row);
}
}import { FieldList, FieldRow, Input } from '@arclux/arc-ui-solid';
import { createSignal, For } from 'solid-js';
export default function Example() {
const [options, setOptions] = createSignal(['Yes', 'No']);
const move = (from, to) => setOptions((o) => { const n = [...o]; n.splice(to, 0, n.splice(from, 1)[0]); return n; });
return (
<FieldList label="Options" addLabel="Add option" min={2} max={6}
onArcAdd={() => setOptions((o) => [...o, ''])}
onArcRemove={(e) => setOptions((o) => o.filter((_, i) => i !== e.detail.index))}
onArcMove={(e) => move(e.detail.from, e.detail.to)}>
<For each={options()}>{(value, i) => (
<FieldRow label={`Option ${i() + 1}`}>
<Input label={`Option ${i() + 1}`} value={value} name="option" />
</FieldRow>
)}</For>
</FieldList>
);
}import { FieldList, FieldRow, Input } from '@arclux/arc-ui-preact';
import { useState } from 'preact/hooks';
export default function Example() {
const [options, setOptions] = useState(['Yes', 'No']);
const move = (from, to) => setOptions((o) => { const n = [...o]; n.splice(to, 0, n.splice(from, 1)[0]); return n; });
return (
<FieldList label="Options" addLabel="Add option" min={2} max={6}
onArcAdd={() => setOptions((o) => [...o, ''])}
onArcRemove={(e) => setOptions((o) => o.filter((_, i) => i !== e.detail.index))}
onArcMove={(e) => move(e.detail.from, e.detail.to)}>
{options.map((value, i) => (
<FieldRow key={i} label={`Option ${i + 1}`}>
<Input label={`Option ${i + 1}`} value={value} name="option" />
</FieldRow>
))}
</FieldList>
);
}API
labelstring''- Accessible name for the group of rows, such as "Options".
add-labelstring'Add'- Text of the Add button, exactly as given. The plus sign before it is an icon, not part of the text, so
add-label="Option"reads "Option" with a + beside it. readonlybooleanfalse- Show the rows without their controls: no Add button, no handles, no Remove buttons. For a fixed set that should look like the editable one, such as a taught question's options.
minnumber0- The fewest rows allowed. Remove is disabled at this count.
maxnumber0- The most rows allowed. Add is disabled at this count. Unset is unlimited.
Events
arc-add- The user asked for a new row. Append one to your array; focus moves into it when it renders.
arc-removedetail:{ value: number, index: number }- The user asked to remove the row at
index. Remove it from your array. arc-movedetail:{ value: number, from: number, to: number }- The user moved the row at
fromtoto(alsovalue). Move it in your array; focus follows it.
FieldRow
<arc-field-row>One row: your inputs, with a handle to reorder the row and a button to remove it.
labelstring''- What this row is called in its controls' names, such as "Option 2". Defaults to "Row" and its position.
Methods
focusFirstField()- Focus the first field in the row, as after it was just added.
focusHandle()- Focus the handle, as after the row was moved.
See Also
- Sortable ListDrag-and-drop reorderable list with grip handles, keyboard reordering support, and visual insertion indicators.
- FormForm wrapper with built-in validation, error aggregation, and submit handling. Composes Input, Textarea, and Button into a cohesive data-entry workflow.
- InputVersatile form control supporting single-line text, email, password, and multiline textarea modes with built-in label, placeholder, and validation states. Pairs with Form for complete data-entry workflows.