Form
The wrapper every form in the app is built on: pending state, the submit button, and three separate error channels. It also owns the captcha flow — for the three forms that need one.
import Form from '@/components/ui/Form';Source: src/components/ui/Form.tsx
Examples
Three error channels, and they are not interchangeable
fieldErrors land inline on the control, next to what is wrong. formError renders an Alert above the submit button — for something about the submission as a whole that the user can fix and retry, like a wrong password. error opens a modal, for a genuine failure. Putting a mistyped password in the modal channel is what made every failed login say “Jotain meni vikaan”, which both misdescribed it and cost a dismissing click before the retry.
formError renders an Alert here, directly above the button — see the Alert page for how that looks.
captchaAction is optional, deliberately
Only login, signup and password reset pass it — those submit to api-users, which verifies the token. Every authenticated mutation omits it: that API does not check captcha tokens at all, so obtaining one buys nothing, while the v2 fallback can actively harm — a user whose reCAPTCHA is blocked by an adblocker would be gated behind an image challenge to change their own email address. This is a documented deviation from ui/DESIGN.md line 195. Don’t “restore” it.
Omitting the prop skips the Google script, the token fetch and the fallback entirely. The props table below shows it as optional because the type is a union of two shapes — with a captcha and without.
Props
<Form> props
These props come in alternative sets
The type is a union, so some combinations below are not valid together — passing one prop can require or exclude another. The component file states which.
| Prop | Type | Default | Description |
|---|---|---|---|
captchaAction | string | — | reCAPTCHA action name, e.g. "login" / "signup" / "password_reset". Must match the action the server verifies the token against. |
onSubmitrequired | ((captcha: CaptchaToken) => Promise<FormResult | void>) | (() => Promise<FormResult | void>) | — | Called with a fresh captcha token. Return a FormResult, or nothing on success. |
submitLabelrequiredfrom FormBaseProps | string | — | — |
pendingLabelrequiredfrom FormBaseProps | string | — | Submit-button label while the submission is in flight. |
childrenrequiredfrom FormBaseProps | ReactNode | ((state: FormState) => ReactNode) | — | The form's controls. |
footerfrom FormBaseProps | ReactNode | — | Rendered below the submit button, e.g. a "already have an account?" line. |
secondaryActionfrom FormBaseProps | ReactNode | — | A second control sharing the submit button's row — a "back" button on a multi-leg form, say. Both size to their own label, as the withdrawal flow's footer does, and the row wraps when the two no longer fit side by side. The submit button only takes the full width when it stands alone. |
secondaryPlacementfrom FormBaseProps | 'inline' | 'opposite' | 'inline' | Where `secondaryAction` sits relative to the submit button. `'inline'` (the default) keeps the two adjacent with the secondary first — a Back button belongs beside the action it steps away from, as PhoneForm's does. `'opposite'` pushes them to opposite ends of the row, submit first: the arrangement for a control that is not part of the same progression, like a "discard this whole attempt" link, where sitting flush against the primary button would invite a mis-click. |
submitWidthfrom FormBaseProps | 'full' | 'auto' | 'full' | How wide the submit button is when it stands alone. `'full'` (the default) is the app's form look: one button spanning the card. `'auto'` sizes it to its own label, for a form whose legs otherwise disagree with themselves — the phone form's first leg would jump from a full-width button to a narrow one as soon as the code field appears beside a Back button. Ignored when `secondaryAction` is set: two buttons sharing a row are always label-width. |
classNamefrom FormBaseProps | string | '' | — |
Usage
Do
- Wrap each control in a Field, and stack the Fields with a Stack.
- Pick the error channel by what the user can do about it, not by severity.
Don’t
- Don’t add captchaAction to a settings or review form. See above — it is not an oversight.
- Don’t roll your own pending state; the render prop gives you the form’s.