A reactive form management library for SvelteKit with type-safe validation, built-in accessibility, and automatic error handling using Svelte 5's latest features.
fixed()
methodnpm install @git-franckg/sveltekit-forms
# or
yarn add @git-franckg/sveltekit-forms
# or
pnpm add @git-franckg/sveltekit-forms
// +page.ts
import { Form } from '@git-franckg/sveltekit-forms';
import { object, string, pipe, email, minLength } from 'valibot';
export const load = () => {
const form = new Form(
{
schema: object({
email: pipe(string(), email()),
password: pipe(string(), minLength(8))
}),
behavior: {
email: {},
password: {}
}
},
{ email: '', password: '' },
(output) => {
console.log('Form submitted:', output);
}
);
return { form };
};
<!-- +page.svelte -->
<script lang="ts">
const { data } = $props();
$effect(() => data.form.tick());
</script>
<form>
<label {@attach data.form.behaviors.email.label}>Email</label>
<input bind:value={data.form.input.email} {@attach data.form.behaviors.email.input} />
<p {@attach data.form.behaviors.email.error}>
{data.form.behaviors.email.issueText?.[0]}
</p>
<button type="button" onclick={data.form.submit}>Submit</button>
</form>
graph TB
subgraph "Form Instance"
Form["Form<T><br/>• input: T (reactive state)<br/>• behaviors: Record<key, Behavior><br/>• issues: FlattenedIssues | null"]
end
subgraph "Validation Flow"
Schema["Standard Schema<br/>(Valibot, Zod, etc.)"]
Validate["validate(input)"]
Issues["Issues<br/>• formIssues[]<br/>• fieldIssues{}"]
end
subgraph "Behavior System"
Behavior["Behavior<br/>• issueText<br/>• issueShown<br/>• touched"]
Attachments["Attachments<br/>• input()<br/>• label()<br/>• error()<br/>• caption()"]
DOM["DOM Elements<br/>with ARIA attributes"]
end
subgraph "User Interaction"
UserInput["User Input"]
Events["Events<br/>• oninput<br/>• onchange<br/>• onblur"]
end
Form -->|"$derived"| Validate
Schema --> Validate
Validate --> Issues
Issues -->|"tick()"| Behavior
Behavior --> Attachments
Attachments -->|"@attach"| DOM
UserInput --> Events
Events -->|"Update"| Form
Events -->|"Toggle visibility"| Behavior
Form -->|"submit()"| Schema
Schema -->|"Valid"| Success["onSubmit callback"]
Schema -->|"Invalid"| ShowErrors["Show all errors"]
style Form fill:#e1f5fe
style Behavior fill:#f3e5f5
style Schema fill:#e8f5e9
style DOM fill:#fff3e0
sequenceDiagram
participant User
participant Input as Input Field
participant Form as Form State
participant Schema as Validation Schema
participant Behavior as Field Behavior
participant UI as UI Feedback
User->>Input: Types in field
Input->>Form: Updates input value
Input->>Behavior: Triggers oninput
Behavior->>UI: Hides error (issueShown = false)
Form->>Schema: Validates continuously ($derived)
Schema->>Form: Returns validation result
Form->>Behavior: Updates issueText via tick()
User->>Input: Leaves field (blur)
Input->>Behavior: Triggers onblur
Behavior->>UI: Shows error if touched
User->>Input: Changes value
Input->>Behavior: Triggers onchange
Behavior->>UI: Shows error (issueShown = true)
User->>Form: Clicks submit
Form->>Form: Checks for issues
alt Has validation errors
Form->>Behavior: Mark all fields as touched & shown
Behavior->>UI: Display all errors
else No errors
Form->>User: Call onSubmit callback
end
class Form<T extends FormInput> {
input: T; // Reactive form data
behaviors: Record<keyof T, Behavior>; // Field behaviors
issues: FlattenedIssues<T> | null; // Current validation errors
constructor(config: FormConfig<T>, initialValue: T, onSubmit: (output: T) => void);
submit(): void; // Validate and submit form
tick(): void; // Update field errors
fixed<TFixed>(fixedInput: TFixed): Form<Extract<T, TFixed>>;
}
The Behavior class manages individual field UI behavior and accessibility:
aria-invalid
, aria-errormessage
, etc.Use the {@attach}
directive to connect behaviors to DOM elements:
<label {@attach form.behaviors.email.label}>Email</label>
<input {@attach form.behaviors.email.input} />
<span {@attach form.behaviors.email.caption}>Optional helper text</span>
<p {@attach form.behaviors.email.error}>Error message here</p>
type FormInput =
| { type: 'login'; email: string; password: string }
| { type: 'register'; email: string; password: string; confirmPassword: string };
// In your component
const form = data.form.fixed({ type: 'register' });
// Now TypeScript knows about confirmPassword field
const schema = object({
email: pipe(string('Email is required'), email('Please enter a valid email'))
});
// Update a field value
form.input.email = '[email protected]';
// Mark field as touched
form.behaviors.email.touched = true;
form.behaviors.email.issueShown = true;
Requires browsers with support for:
Contributions are welcome! Please feel free to submit a Pull Request.
MIT
Built with Svelte 5 and Standard Schema.