Components
Form
A native <form> that hands its fields the server's errors, after Base UI's Form. Pass the data a form action gives fail() as errors, and each Field shows the message for its name, in the HTML the server renders back (no JavaScript needed) or, with submit(), in an island. Submission, validation and the first invalid field's focus are the platform's and Field's.
import { Form } from "@lumiose/ui"; // or "@lumiose/ui/form"#Anatomy
<Form>
<Field.Root />
</Form>| Part | Renders |
|---|---|
Form |
<form>, with the props it is given. |
#API
#Form
| Prop | Type | Default | |
|---|---|---|---|
errors |
object |
The server's errors by field name: a message or several for each Field.Root name, such as actionData() after a fail(). |
|
validationMode |
"onSubmit" | "onBlur" | "onChange" |
"onSubmit" |
When the fields show their errors, unless a field sets its own. |
values |
FormData, URLSearchParams or { [name]: string | string[] } |
The values to fill the controls with after a failed submission, by name, such as the record of fields an action passes back with fail(). |
Every other <form> prop passes through: method, action, onSubmit (Lumiose's submit),
novalidate, class.
values replace the controls' defaults, by name (the control's, or its Field's): the text of
an Input (Field.Control), an Autocomplete, a Select, a Number Field or a Slider, the checked state of
a Checkbox or Switch (a name the submission left out means unchecked), and a Checkbox Group's,
Radio Group's or Combobox's selection. Controls with a value (or bind:) keep theirs, and
passwords, files, hidden inputs (whatever the case of their type), OTP Fields and disabled
controls (also in a disabled Fieldset.Root) are never filled. So after a failed action the page
renders the form as the user left it, with JavaScript or without (WCAG 3.3.7, redundant entry).
Values match by name only: give a password and a text field different names, or the text field gets
the password. A native <fieldset disabled> is not seen while rendering, so its controls are
filled; use Fieldset.Root disabled instead.
Differences from Base UI's Form. No onFormSubmit or actionsRef: the form is native and
Lumiose's form actions post it (action="?/name", onSubmit={submit}). Base UI sets
noValidate; here the browser's validation stays on, so it still works without JavaScript, and
Field's script shows the problems in the fields instead of the browser's bubble.
#Example
A sign-up page whose action checks the username on the server:
import { Field, Form } from "@lumiose/ui";
import { actionData, fail, type ActionContext } from "lumiose";
export const actions = {
async signup(ctx: ActionContext) {
const form = await ctx.formData();
const username = String(form.get("username") ?? "");
if (username === "admin") {
// The errors, and the fields to fill in again (never a password): the form renders back
// filled in.
return fail(400, { errors: { username: "That username is taken." }, values: { username } });
}
return { welcome: username };
},
};
export function SignupForm() {
const result = actionData<typeof actions>();
const failed = result && "errors" in result ? result : undefined;
return (
<Form method="post" action="?/signup" errors={failed?.errors} values={failed?.values}>
<Field.Root name="username" class="lu-field">
<Field.Label>Username</Field.Label>
<Field.Control required minlength={3} autocomplete="username" class="lu-input" />
<Field.Error />
</Field.Root>
<button class="lu-button" data-variant="primary">
Sign up
</button>
</Form>
);
}Only the keys of errors that name a field and hold a string or an array of strings become
errors, so errors={result} also works for an action that fails with the errors alone. The
values reach the browser with the page, so send back the fields to fill in by name, as above. The
submitted FormData works too (its text entries; files are left out), but it carries every field,
a password included: Form never fills a password field, yet the password would sit in the page's
HTML. In development, Lumiose warns when a FormData it serializes carries a name that looks
secret. In an island, add onSubmit={submit} and the errors arrive without a page load; the
controls keep what the user typed anyway.
#Styling
Form adds nothing to style: style the form and its state, form:invalid (a field is invalid)
and form:has(:user-invalid) (one the user touched is), and its fields
(Field). Form ships no stylesheet.
A form reset (a type="reset" button, form.reset()) puts every control back to its default
and reports it: onValueChange / onCheckedChange get the reset value a moment later, when it
differs, so bind: signals in an island follow the form (the platform fires no input or
change for a reset).
#Keyboard and focus
The platform's: Enter in a text field submits the form. On a submit attempt with invalid fields, nothing is sent, the fields show their errors, and the first invalid control takes focus (Field's script; without it, the browser focuses it and shows its bubble).
#Without JavaScript
A native form posts to its action and the page renders back with actionData(), so the server's
errors show in their fields with no script, and with values the controls show what was
submitted. The browser validates the fields before it posts.
#Browser support
Every browser. Field's script needs the invalid event and constraint validation, which every
supported browser has.
#Related
- Field: the fields and their errors.
- Fieldset: groups of fields.
- Data and actions: form actions,
fail(),actionData()andsubmit().