Docs
Formsnap & Superforms

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 the bits-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 directly as our page component, but you can put it anywhere you like.

src/routes/settings/schema.ts
	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

src/routes/settings/+page.server.ts
	import type { PageServerLoad } from "./$types";
import { superValidate } from "sveltekit-superforms/server";
import { formSchema } from "./schema";

export const load: PageServerLoad = () => {
  return {
    form: 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.

src/routes/settings/settings-form.svelte
	<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.

src/routes/settings/+page.svelte
	<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

src/routes/settings/+page.server.ts
	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 = () => {
  return {
    form: 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.

This is your public display name.

Examples

See the following links for more examples on how to use the other Form components: