Skip to content

Form

A form wrapper that renders an accessible error summary and moves focus on failed submits, while staying out of the way of native submission and SvelteKit's use:enhance.

Import

import { Form, toFormErrors } from "@hyzer-labs/ui"

Demo

The default path: omit onSubmit and the Form never touches the submit. SvelteKit's use:enhance does the work (attached with fromAction, Svelte 5.32+), and toFormErrors maps the action's zod-flattened errors straight into the summary. The demo is live on the real enhance wiring; only the server hop is simulated, since this docs site is prerendered. It runs the same schema as the action below, with about 400ms of latency. Submit it empty: the errors arrive asynchronously, and the summary takes focus. On success the form resets, which is enhance's default.

<!-- +page.svelte -->
<script>
	import { enhance } from '$app/forms';
	import { fromAction } from 'svelte/attachments';
	import { Form, TextInput, Button, toFormErrors } from '@hyzer-labs/ui';

	let { form } = $props();
	const errors = $derived(toFormErrors(form?.errors));

	// Pending state for the submit button, tracked in the enhance submit hook.
	let pending = $state(false);
	const submit = () => {
		pending = true;
		return async ({ update }) => {
			pending = false;
			await update();
		};
	};
</script>

<Form method="POST" {errors} summaryHeadingLevel={3} ariaLabel="League signup" {@attach fromAction(enhance, () => submit)}>
	<Stack gap="md">
		<TextInput name="player" label="Player name" />
		<TextInput name="email" label="Email" type="email" />
		<Button type="submit" loading={pending}>Sign up</Button>
	</Stack>
</Form>

The action this demo simulates, with validation on the server where it belongs:

// +page.server.ts
import { fail } from '@sveltejs/kit';
import { z } from 'zod';

const schema = z.object({
	player: z.string().min(1, 'Enter your name.'),
	email: z.email('Enter a valid email address.')
});

export const actions = {
	default: async ({ request }) => {
		const data = Object.fromEntries(await request.formData());
		const result = schema.safeParse(data);
		if (!result.success) return fail(400, { errors: z.flattenError(result.error) });
		// save result.data, fully typed
		return { success: true };
	}
};

Props

NameTypeDefaultNote
errorsFormError[][]The summary renders when non-empty. See FormError below.
onSubmit(e: SubmitEvent) => voidOmit for native submission / use:enhance (the default path); provide it for client-side validation — preventDefault runs first.
summaryTitlestring | ((count: number) => string)'There is a problem'A string overrides the default outright; a function receives the error count. The default is count-aware and plural-safe: "There is a problem" for 1, "There are 3 problems" for 3.
summaryHeadingLevel2 | 3 | 4 | 5 | 62Match the page's heading structure.
focusTarget'summary' | 'firstField''summary'What receives focus after a submit with errors.
novalidatebooleanfalse
ariaLabelstringNames the form landmark.
childrenSnippetRequired. The form content.
classstringMerged after the hz-form class.

FormError

NameTypeDefaultNote
namestringField name to link to. Empty or unresolved names become non-interactive form-level errors, listed last.
messagestringRequired.

Theme hooks

What this component promises your CSS. The reference theme styles exactly these — from @layer hz-theme, so your unlayered rules win. See Styling Components for the how.

Root class: .hz-form

Data attributes

HookValuesStyles
data-state'error' | absentPresent once the summary has errors. Two-state, unlike the field family’s three — a form is never disabled.

Part classes

HookValuesStyles
.hz-form-error-summaryon an AlertThe summary box — an Alert underneath. Load-bearing beyond styling: the focus-on-submit path queries this class.
.hz-form-error-listchild elementThe list of errors.
.hz-form-error-summary-itemchild elementOne error row, including its jump link.

Accessibility

The error summary is the first child of the form and has role="alert", so it is announced when it appears. It takes focus when errors arrive after a submit. With focusTarget="firstField", the first invalid field takes focus instead.

Each summary item links to its field and focuses it on activation, scrolling smoothly unless prefers-reduced-motion is set.

Set summaryHeadingLevel so the summary title fits your page's heading outline.

References: MDN: <form>