Async submission
Return a promise from onSubmit when your submit flow is asynchronous.
async function onSubmit(values: LoginValues) { const response = await api.login(values); if (!response.ok) { throw new Error('Invalid credentials'); }}While the promise is pending, the form sets isProcessing to true and submit buttons mark
themselves aria-disabled automatically, which blocks activation without dropping the focus of the
user who pressed the button. The button also shows a spinner, positioned inside its own padding so
the button does not change size mid-submit; it collapses to a static dot under
prefers-reduced-motion.
The form also announces the in-flight state to assistive technology through a polite live region, so
pressing submit is not met with silence. It is screen-reader-only — the dimmed button is already the
visual signal — and it says Submitting… by default. Reword it when that is not the action, or pass
an empty string to stay silent:
<Form onSubmit={onSubmit} processingLabel='Signing in…'>Rejected submissions
Section titled “Rejected submissions”If the handler throws or its returned promise rejects, isProcessing is reset in a finally block.
The error message is added to form.state.errors for display.
Each new submit attempt clears existing form.state.errors before calling the handler, so stale
server errors do not linger after a retry begins.
Edits during an in-flight submit
Section titled “Edits during an in-flight submit”Fields stay editable while a submit is in flight — locking the form would take focus away from
whatever the user is typing in. With a schema, that creates a race: validation runs against the
values captured when submit was pressed, so if any of them change before it resolves, the result
describes a form that no longer exists. That submit is discarded rather than sent, and
form.state.errors reports The form changed while it was being submitted. Please submit again. so
the discard is visible instead of looking like a button that did nothing.
Without a schema there is no async gap to race, and the handler receives the values captured at
press time.
Work the form does not run itself
Section titled “Work the form does not run itself”Everything above happens automatically for work awaited by onSubmit. When the async work is
somewhere else — a router action, a mutation, a resource loading the form’s initial values — tell
the form about it with isProcessing and isLoading:
<Form onSubmit={onSubmit} isLoading={user.loading} isProcessing={saveUser.pending}>isLoading disables every registered field and gives every submit button the same unavailable
treatment a real submit gets — the spinner, aria-disabled, the blocked activation — so partially
loaded values can never reach onSubmit. It announces Loading… in the form’s status region;
override that with loadingLabel or pass '' to stay silent. isProcessing gives you that same
treatment while an actual submit is in flight, with its own processingLabel announcement. Because
no button initiated either kind of work, every submit button shows the spinner — a real submit
shows it only on the button that was pressed.
Both are additional sources rather than overrides — the form is processing when you say so or
when it is running a submit of its own. Passing isProcessing={false} therefore cannot cut a real
submit short. That is deliberate: the same flag guards against a second submit starting on top of
one already in flight, so a false that won outright would let a double submit through.
Reading processing state
Section titled “Reading processing state”Use useForm when you want to show state outside the form.
const form = useForm<LoginValues>();
<Show when={form.state.isProcessing}>Signing in...</Show>;
<form.Form onSubmit={onSubmit}> <InputField name='email' type='email' label='Email' required /> <PasswordField name='password' label='Password' required /> <SubmitButton>Log in</SubmitButton></form.Form>;Naming submit buttons for more than one action
Section titled “Naming submit buttons for more than one action”onSubmit can be an object instead of a single function — a map from a button’s name to the
handler that should run when that button submits the form. Give each SubmitButton a matching
name and the right handler runs, whichever one the user pressed.
Press either button below and watch which handler reports back — both are async, so there is a
moment to see which one is actually running.
import { SubmitButton, type SubmitResponseMapping, createForm } from 'solid4m';
interface PostValues { title: string;}
const { Form, InputField } = createForm<PostValues>();
async function saveDraft(values: PostValues) { await api.saveDraft(values);}
async function publish(values: PostValues) { await api.publish(values);}
function PostForm() { return ( <Form<PostValues, SubmitResponseMapping<PostValues>> onSubmit={{ saveDraft, publish }}> <InputField name='title' label='Title' required /> <SubmitButton name='saveDraft'>Save draft</SubmitButton> <SubmitButton name='publish'>Publish</SubmitButton> </Form> );}The second type argument, SubmitResponseMapping<PostValues>, is required for a map: TypeScript
cannot work out from the object literal alone that onSubmit is a map rather than a single handler.
With useForm, pass it there instead: useForm<PostValues, SubmitResponseMapping<PostValues>>().
Each handler is optionally passed a second argument: the name of the button that triggered it.
type OnSubmitHandler<P, R> = (values: P, buttonName: string) => Promise<R> | void;That second argument matters when several buttons intentionally share one handler — it is how the handler tells them apart without needing a separate function per button.
A map with exactly one handler needs no matching name on its button at all. With nothing to
disambiguate, that single handler runs regardless of whether the button that submitted has a name,
or any name at all — silently doing nothing would be worse than running the one handler there is.
With two or more handlers, a button that submits without a matching name — unnamed, or named
something not in the map — can’t be resolved. Nothing runs: no handler is called, no error is set, and
the form looks like it did nothing. That silence is exactly what this library tries to avoid
everywhere else, so it is not silent here either — a console.warn names the problem and lists the
handlers that were available, so the fix (add or correct a name) is visible in development instead
of discovered from a support ticket.
The in-flight treatment from the top of this page still applies per button: while either handler is
running, every SubmitButton in the form goes aria-disabled — a second submit really is
unavailable while one is in flight — but only the button that was actually pressed shows its spinner,
so the form never claims two actions are running when only one is.
Next steps
Section titled “Next steps”See Accessibility for how the in-flight and invalid-submit treatment fits into the rest of the form’s focus and live-region behavior.