Formsnap & Superforms
Building forms with Formsnap, Superforms, & Zod.
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
Well-designed HTML forms are:
- Well-structured and semantically correct.
- Easy to use and navigate (keyboard).
- Accessible with ARIA attributes and proper labels.
- Has support for client and server side validation.
- Well-styled and consistent with the rest of the application.
In this guide, we will take a look at building forms with formsnap, sveltekit-superforms and zod.
Features
The Form
components offered by shadcn-svelte
are wrappers around formsnap
& sveltekit-superforms
which provide a few things:
- Composable components for building forms.
- A
<Form.Field />
component for building controlled form fields. - Form validation using
zod
. - Applies the correct
aria
attributes to form fields based on states. - Enables you to easily use various components like Select, RadioGroup, Switch, Checkbox and other form components as form fields.
- Provides an optional native
<select />
&<input type="radio" />
with out of the box functionality if you prefer to use native form elements rather than thebits-ui
components.
If you aren't familiar with Superforms & Formsnap, you should check out their documentation first, as this guide assumes you have a basic understanding of how they work together.
Anatomy
<Form.Root>
<Form.Field>
<Form.Item>
<Form.Label />
<!-- Any Form input component -->
<Form.Description />
<Form.Validation />
</Form.Item>
</Form.Field>
</Form.Root>
Example
<Form.Root {schema} {form} let:config>
<Form.Field {config} name="email">
<Form.Item>
<Form.Label />
<Form.Input />
<Form.Description />
<Form.Validation />
</Form.Item>
</Form.Field>
</Form.Root>
Installation
npx shadcn-svelte@latest add form
Usage
Create a form schema
Define the shape of your form using a Zod schema. You can read more about using Zod in the Zod documentation. We're going to define it in a file called schema.ts
in the same directory as our page component, but you can put it anywhere you like.
import { z } from "zod";
export const formSchema = z.object({
username: z.string().min(2).max(50),
});
export type FormSchema = typeof formSchema;
Return the form from the route's load function
import type { PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms/server";
import { formSchema } from "./schema";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(formSchema),
};
};
Create a form component
For this example, we'll be passing the form
returned from the load function as a prop to this component. To ensure it's typed properly, we'll use the SuperValidated
type from sveltekit-superforms
, and pass in the type of our form schema.
<script lang="ts">
import * as Form from "$lib/components/ui/form";
import { formSchema, type FormSchema } from "./schema";
import type { SuperValidated } from "sveltekit-superforms";
export let form: SuperValidated<FormSchema>;
</script>
<Form.Root method="POST" {form} schema={formSchema} let:config>
<Form.Field {config} name="username">
<Form.Item>
<Form.Label>Username</Form.Label>
<Form.Input />
<Form.Description>This is your public display name.</Form.Description>
<Form.Validation />
</Form.Item>
</Form.Field>
<Form.Button>Submit</Form.Button>
</Form.Root>
The name
, value
, and all accessibility attributes will be automatically applied to the input thanks to Formsnap.
Create a page component that uses the form
We'll pass the form
from the data returned from the load function to the form component we created above.
<script lang="ts">
import type { PageData } from "./$types";
import SettingsForm from "./settings-form.svelte";
export let data: PageData;
</script>
<SettingsForm form={data.form} />
Create an Action that handles the form submission
import type { PageServerLoad, Actions } from "./$types";
import { fail } from "@sveltejs/kit";
import { superValidate } from "sveltekit-superforms/server";
import { formSchema } from "./schema";
export const load: PageServerLoad = async () => {
return {
form: await superValidate(formSchema),
};
};
export const actions: Actions = {
default: async (event) => {
const form = await superValidate(event, formSchema);
if (!form.valid) {
return fail(400, {
form,
});
}
return {
form,
};
},
};
Done
That's it. You now have a fully accessible form that is type-safe and has client & server side validation.
Options
You can handle the form submission in a few different ways using the options
from formsnap
.
See the documentation for more information.
Click the above Submit button to see the different options in action.
Examples
See the following links for more examples on how to use the other Form
components:
On This Page