Skip to content
TrademarkTrademark
Docs
Form Controls

Form

Updated: September 21, 2026
View on GitHub

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.

Why not assign corners by hand

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.

Density

"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 and spans

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.

Responsive columns

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

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.

Toolbar size

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.

Focus

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.

What fuses, and what does not

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.

Submitting

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.

Building blocks

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.

API reference

Type
Default
density
Gap and grouping as one decision. "stacked" fuses the fields into a single control; "tight" and "spacious" leave each its own border and radius.
stackedtightspacious
tight
columns
Grid columns — a number, or a per-breakpoint map like { xs: 1, m: 2 }.
numberPartial<Record<Breakpoint, number>>
—
size
Size for every field in the group. A field that sets its own size keeps it.
xllmsxsxxs
—
as
Render a real <form> rather than a <div>.
divform
div
onSubmit
Submit handler. Only attached when as is "form". Return a promise and the form is pending until it settles.
(event: React.FormEvent<HTMLFormElement>) => unknown—
pending
Mark the form as submitting from outside. Wins over the state onSubmit tracks.
boolean—
className
string—
style
React.CSSProperties—
children
The fields. Conditional children that render nothing are ignored, and the corners follow.
React.ReactNode—
…and every React.HTMLAttributes propinherited—
Edit this page on GitHub