Form
Installation
npx @mielui/svelte add form field fieldset inputyarn dlx @mielui/svelte add form field fieldset inputbunx @mielui/svelte add form field fieldset inputUsage
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.
<script lang="ts">
import * as Field from '@mielui/svelte/components/field';
import * as Form from '@mielui/svelte/components/form';
import { Input } from '@mielui/svelte/components/input';
let message = $state('');
</script>
<Form.Root method="POST" action="?/save">
<Field.Root required>
<Field.Label>Work email</Field.Label>
<Field.Control>
{#snippet children(control)}
<Input {...control} type="email" name="email" />
{/snippet}
</Field.Control>
</Field.Root>
<Form.Actions>
<Form.Submit>Save</Form.Submit>
</Form.Actions>
<Form.Status>{message}</Form.Status>
</Form.Root>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.
Profile form
A fieldset with two required fields. Submitting shows the pending state, then a status message.
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, such as profile.email or members[0].email; issues without one stay plain text. After a submission finishes with issues, the summary receives focus. Set focusOnError={false} when your application manages focus; errors rendered on the server do not take focus. Pass a heading snippet, or children that receives the issues, to customize the content. To translate the heading text, pass labels={{ heading: 'Vรฉrifiez vos informations' }}; every key of FormErrorSummaryLabels is optional and English is the fallback.
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 app, enable both options in the sveltekit() plugin in vite.config.ts and install Valibot (pnpm add valibot). These examples target Kit 3 and Svelte 5.57.1 or later. See the official remote-functions guide before enabling experimental features.
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
sveltekit({
compilerOptions: {
experimental: {
async: true
}
},
experimental: {
remoteFunctions: true
}
})
]
});Validate at the server boundary
Place the handler in a .remote.ts file under src, outside any server directory. 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.
import { invalid } from '@sveltejs/kit';
import * as v from 'valibot';
import { form } from '$app/server';
export const validateProfile = form(
v.object({
username: v.pipe(
v.string(),
v.trim(),
v.minLength(3, 'Use at least 3 characters.'),
v.maxLength(40, 'Use at most 40 characters.')
),
email: v.pipe(v.string(), v.email('Enter a valid email address.')),
intent: v.picklist(['validate', 'validate-reset'])
}),
async ({ username, intent }, issue) => {
await new Promise((resolve) => setTimeout(resolve, 350));
if (username.toLowerCase() === 'admin') {
invalid(issue.username('This username is reserved. Choose another one.'));
}
if (username.toLowerCase() === 'system') {
invalid('This profile cannot be validated right now. Choose another demo username.');
}
return { username, intent };
}
);
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.
<script lang="ts">
import { enhance } from '$app/forms';
import * as Form from '@mielui/svelte/components/form';
import { fromAction } from 'svelte/attachments';
</script>
<Form.Root method="POST" action="?/save" {@attach fromAction(enhance)}>
<!-- Compose fields here. -->
<Form.Actions>
<Form.Submit>Save</Form.Submit>
</Form.Actions>
</Form.Root>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.
Status and submit states
Status prints a message for the whole form. tone colors it as error, success or neutral, which is the default.
Submit is built on Button and shows the form's progress by itself. Pass status to drive it by hand as idle, loading, success or error, with labels setting the text for each state. size changes its height, unstyled removes the Button classes, and onclick and onkeydown run before the form submits.
API reference
Form.Root
elementBindableHTMLFormElement | nullBindable reference to the DOM element.
pendingnumber | booleanMarks the form as submitting. A number is the count of pending requests.
Default
false
Form.ErrorSummary
labelsFormErrorSummaryLabelsOverrides the built-in text and accessible names. Every key is optional and English is the fallback.
issuesreadonly Readonly<{ message: string; controlId?: string | undefined; path?: readonly (string | number)[] | undefined; }>[]Validation issues to summarize.
Default
[]focusOnErrorbooleanMoves focus to the summary when issues appear.
Default
trueheadingSnippet<[]>Replaces the summary heading.
childrenSnippet<[readonly Readonly<{ message: string; controlId?: string | undefined; path?: readonly (string | number)[] | undefined; }>[]]>Content rendered inside.
elementBindableHTMLDivElementBindable reference to the DOM element.
Form.Status
tone"neutral" | "success" | "error"Color that carries the meaning.
Default
'neutral'
Form.Submit
childrenSnippet<[]>Content rendered inside.
onkeydown((event: KeyboardEvent) => void)Called on key down, before the button handles the key itself.
onclick((event: MouseEvent) => void)Called when the button is activated. Loading and disabled buttons do not call it.
statusButtonStatusControlled visual state. Loading remains focusable and refuses activation.
disabledbooleanPrevents activation. A disabled link loses its destination and is skipped by Tab.
variantButtonVariantVisual style. Choose it for what the action means, not for decoration.
size"sm" | "md" | "lg" | "icon"Control height.
iconis a square button for a single icon.elementBindableHTMLButtonElement | HTMLAnchorElementBindable reference to the rendered DOM element. Type is the union of the two possible element types -- narrow at the use site:
unstyledbooleanSkip the variant/size base classes and render with
classalone.loadingbooleanConvenience alias for
status="loading".labelsButtonLabelsOverrides the built-in text and accessible names. Every key is optional and English is the fallback.
Other parts
These parts have no props of their own. They pass HTML attributes and events to their element.
Form.Actions
pnpm dlx @mielui/svelte add form field fieldset input