Form
Installation
Native first
Root renders a form and forwards native attributes, callbacks, and symbol-keyed attachments. It does not prevent submission, serialize values, swallow errors, or reset inputs. Use a native action, SvelteKit form action, remote form, or your own onsubmit handler. Native validation remains enabled unless you explicitly set novalidate.
Actions is an independent flex container; move it before the fields or omit it entirely. Submit is a native submit button styled with Button and inherits Root’s pending state. It preserves name, value, formaction, formmethod, and other submitter attributes. Status is a polite live region with neutral, success, and error tones. Keep it mounted when its message changes.
Pass pending as a boolean or a pending-request count. It marks the form busy and shows loading feedback on Submit without disabling all fields or removing their values from FormData. Root’s element binding exposes the actual form. Use element.requestSubmit() to preserve validation and submitter behavior; element.submit() bypasses them.
A complete error summary
Pass validation issues directly to ErrorSummary. Each issue needs a message and may include a path or controlId. Paths resolve against native field names within the form; form-level issues remain text. Nested paths use names such as profile.email or members[0].email. After a submission finishes with issues, the summary receives focus. Set focusOnError=false when your application manages focus. Initial server-rendered errors do not steal focus. Override heading or children(issues) to customize the content.
SvelteKit remote forms
This live example uses SvelteKit’s experimental remote-form API. Schema errors appear beside each control. A reserved username is rejected on the server. Validation and network failures keep the draft.
The docs application opts in to remote functions and async compilation. Mielui itself imports no Kit runtime and requires no experimental flag. To run this example in your own Kit 2 application, explicitly enable both options and install Valibot (pnpm add valibot). These examples target Kit 2.70 or later and Svelte 5.39 or later. See theofficial remote-functions guide before enabling experimental features.
Validate at the server boundary
Place the handler in a .remote.ts file under src, outside src/lib/server. Its schema is the authority. Real mutations must authenticate and authorize the current request before changing data. This demonstration waits briefly to make pending feedback visible and returns a validation result; it writes nothing.
Preserve the remote form’s attributes
Spread the remote form or its enhance() result directly onto Root. Its attachment reaches the actual form. Spread fields.username.as('text') onto Input after Field.Control’s attributes, so Kit owns the field name, value, and validity. Do not add a competing bind:value or replace its name. Each rendered form needs its own instance; .for(id) keeps repeated forms independent.
enhance receives a form instance. Await instance.submit(): false means validation failed; thrown errors are transport or application failures. A successful enhanced submission does not reset automatically. Call instance.element.reset() deliberately after success. The examples use toast.promise for pending, success, and failure feedback. Mount Toaster once in your app layout. ErrorSummary focuses the error summary after rendering its issues. The summary resolves issue paths to the form’s named fields; form-level issues remain readable text.
Use field.issues() for local feedback, fields.issues() for form-level issues, and fields.allIssues() for a full summary. Pass local issues to Field.Root and render Field.Error. Pass all issues to Form.ErrorSummary, including root-level issues without a path. Validation is not triggered on every keystroke by Form; choose validate() or preflight(schema) when your application needs that policy.
Files, checkboxes, and multiple actions
Remote files need enctype="multipart/form-data" and fields.file.as('file') on a native file control. Unchecked checkboxes are absent from FormData, so make boolean schema fields optional or defaulted. Mark sensitive fields with Kit’s underscore naming convention, such as _password, so validation round trips do not echo them. Use fields.intent.as('submit', value) on each Submit to preserve which action was chosen.
Multiple actions and reset
For server validation returned without JavaScript, declare stable describedBy and errorId values on Field.Control and matching IDs on Description and Error. The multiple-action example uses $props.id() so those relationships work before hydration. The basic example lets Field associate descriptions and errors after hydration.
Use submitter values when the server needs to distinguish actions. This example resets only after successful validation; errors preserve the draft.
Stable SvelteKit form actions
Ordinary form actions need no experimental flag. A named POST action continues to work without JavaScript. Add progressive enhancement with fromAction; the attachment passes through Root. Keep your +page.server.ts action responsible for validation and persistence.
Layout and feedback ownership
Compose Fieldset for named groups and Field for individual controls. Root supplies a vertical layout that class can replace; class="contents" lets a dialog’s form fields and footer participate in the surrounding layout. Keep dialog headings outside that form when they belong to the dialog itself. There is no hidden validation store, submission timer, or global form state.
API reference
Form.Root
| Prop | Type | Default |
|---|---|---|
element Bindable | HTMLFormElement | null | undefined | — |
pending | number | boolean | undefined | false |
Form.Actions
Form.ErrorSummary
| Prop | Type | Default |
|---|---|---|
issues | readonly Readonly<{ message: string; controlId?: string | undefined; path?: readonly (string | number)[] | undefined; }>[] | undefined | [] |
focusOnError | boolean | undefined | true |
heading | Snippet<[]> | undefined | — |
children | Snippet<[readonly Readonly<{ message: string; controlId?: string | undefined; path?: readonly (string | number)[] | undefined; }>[]]> | undefined | — |
element Bindable | HTMLDivElement | undefined | — |
Form.Status
| Prop | Type | Default |
|---|---|---|
tone | "error" | "success" | "neutral" | undefined | 'neutral' |
Form.Submit
| Prop | Type | Default |
|---|---|---|
children | Snippet<[]> | undefined | — |
onkeydown | ((event: KeyboardEvent) => void) | undefined | — |
onclick | ((event: MouseEvent) => void) | undefined | — |
status | ButtonStatus | undefined Controlled visual state. Loading remains focusable and refuses activation. | — |
disabled | boolean | undefined | — |
variant | ButtonVariant | undefined | — |
size | "sm" | "md" | "lg" | "icon" | undefined | — |
element Bindable | HTMLButtonElement | HTMLAnchorElement | undefined Bindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site: | — |
unstyled | boolean | undefined Skip the variant/size base classes and render with `class` alone. | — |
loading | boolean | undefined Convenience alias for `status="loading"`. | — |
loadingLabel | string | undefined | — |
successLabel | string | undefined | — |
errorLabel | string | undefined | — |
pnpm dlx @mielui/svelte add form field fieldset input