The Form component lays a set of fields out and decides how they read together. density carries the gap and the grouping as one choice, because they are one choice: "stacked" fuses the fields into a single control, while "tight" and "spacious" leave each one its own.
A stacked group is often built by passing corners="top", "none" and "bottom" down a list. That is positional bookkeeping, and it breaks the moment the list changes: hide the third of four fields behind a condition and the group ends on a square edge, because the field that was told it was in the middle is now last.
Form works the other way round. It looks at which children actually rendered, works out where each one lands in the grid, and rounds only the corners that end up on the outside of the group. Nothing has to be told its position.
"stacked" collapses the gap to nothing and pulls adjacent borders onto a single shared hairline. "tight" and "spacious" are ordinary gaps, and every field keeps its own border and radius.
columns lays the fields out in a grid. A field can take more than one column with span, which Form reads off the child and strips before it reaches the DOM.
A last row that does not fill is handled too. Three fields in two columns make an L, and the second field keeps its bottom-right corner because that corner is on the outside of the group — even though it is not the last child.
columns also takes a per-breakpoint map. The column count changes with the viewport, and so does which field sits in a corner — so Form works the placement out for every breakpoint and lets CSS pick. Nothing is measured and nothing re-renders on resize.
size sets the size of every field in the group. A field that sets its own size keeps it. It travels by context rather than being cloned onto the children, so a child that is not a field picks up nothing.
size="xxs" makes every field 32px, the height of a size="s" Button, and rounds a stacked group at radius-m to match. Labels show as placeholders at this size.
In a stacked group the fields overlap by a pixel, so a focused field has to paint above its neighbours — all four of them in two dimensions. Form lifts the focused cell for you. Turn the ring itself on with focusRing on the fields, which is worth doing for a form a keyboard user has to get through.
Form rounds the corners of children that mark their own bordered box as a fusable surface. Input, Textarea and the fields built on them do. Anything else is laid out in the grid and spaced normally, but keeps its own corners — so a Button or a block of text can sit in a form without being absorbed into the group.
Pass as="form" to render a real <form> and take onSubmit. Return a promise from onSubmit and the form is pending until it settles: it is marked aria-busy, a SubmitButton inside it shows a spinner, and a second submit is ignored. For a submit that does not run through onSubmit, pass pending.
Form lays fields out. The pieces around them are separate components that work inside it:
- Field puts a label, hint and error around any control and wires its ids and aria attributes.
- Fieldset names a section of the form and lays out its own group of fields.
- SubmitButton follows the form's pending state.
- FormErrorSummary lists every error after a failed submit and links to each field.