Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Docs

Formisch for React

Build forms in React using Formisch and Valibot.

Framework
UI library

This guide will cover building forms using the Field component, adding schema validation with Valibot (opens in a new tab), handling errors, ensuring accessibility, and more.

Demo

We’ll build a form with a text input and textarea. When you submit, the form data is validated and any errors will be shown.

Approach

This form uses Formisch for state and Valibot for validation. We'll build forms using the Field component, which gives you complete flexibility over the markup and styling.

  • Uses Formisch's useForm hook for form state management.
  • Uses the Form component for submit handling.
  • Import Formisch’s Field under an alias (FormischField) for controlled inputs.
  • Uses shadcn.io Field components for building accessible forms.
  • Uses client-side validation by passing your Valibot schema into schema.

Anatomy

Typical structure: wrap each field with FormischField, and the Field component.

<Form of={form} onSubmit={onSubmit}>
  <FieldGroup>
    <FormischField of={form} path={["title"]}>
      {(field) => (
        <Field invalid={Boolean(field.errors?.length)}>
          <FieldLabel htmlFor="title">Bug Title</FieldLabel>
          <Input {...field.props} id="title" value={field.input} />
           <FieldDescription>
            Provide a concise title for your bug report.
          </FieldDescription>
          <FieldError>{field.errors?.[0]}</FieldError>
        </Field>
      )}
    </FormischField>
  </FieldGroup>
  <Button type="submit">Submit</Button>
</Form>

Form

Create a schema

Define your form shape with a Valibot schema.

Setup

  • Create the form with useForm from Formisch and pass your schema to the schema option,
  • Wrap fields in Form and pass the form to the onSubmit option.
form.tsx
import { Form, Field as FormischField, useForm } from "@formisch/react";
import * as v from "valibot";

const formSchema = v.object({
  // ...
});

export const BugReportForm = () => {
  const form = useForm({
    schema: formSchema,
    initialInput: {
      title: "",
      description: "",
    },
  });

  return (
    <Form of={form} onSubmit={(output) => console.log(output)}>
      {/* Build the form here */}
    </Form>
  );
};

Build

Build the form using the FormischField from Formisch and the shadcn.io Field.

Done

That's it. You now have a fully accessible form with client-side validation.

When you submit the form, the onSubmit handler on Form receives validated output. If the form data is invalid, Formisch will display the errors on field.errors for FieldError.

Validation

Client-side

Formisch validates your form data using the Valibot schema. Define a schema and pass it to the schema option of the useForm hook.

example-form.tsx
import { useForm } from "@formisch/react"
import * as v from "valibot"

const formSchema = v.object({
  title: v.string(),
  description: v.optional(v.string()),
})

export const ExampleForm = () => {
  const form = useForm({
    schema: formSchema,
    initialInput: {
      title: "",
      description: "",
    },
  })
}

Modes

Configure when validation runs via the validate and revalidate options:

form.tsx
const form = useForm({
  schema: formSchema,
  validate: "submit",
  revalidate: "input",
})
OptionRole
"initial"Validation triggers on initial render.
"touch"Validation triggers on field touch.
"input"Validation triggers on field input.
"change"Validation triggers on field change.
"blur"Validation triggers on field blur.
"submit"Validation triggers on form submit.

Displaying Errors

Display errors next to the field using FieldError. For styling and accessibility:

  • Add the invalid prop to the Field component.
  • Don't need to add the invalid prop to the form control such as Input, Checkbox, etc.
form.tsx
<FormischField of={form} path={["email"]}>
  {(field) => (
    <Field invalid={Boolean(field.errors?.length)}>
      <FieldLabel htmlFor="email">Email</FieldLabel>
      <Input {...field.props} id="email" type="email" value={field.input} />
      <FieldError>{field.errors?.[0]}</FieldError>
    </Field>
  )}
</FormischField>

Different types of fields

Input

  • Spread field.props on Input and set value={field.input}.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Textarea

  • Spread field.props on Textarea and set value={field.input}.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

NativeSelect

  • Spread field.props on NativeSelect and set value={field.input}.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Select

  • Wire field.input and field.onChange to Select.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Checkbox

  • Wire field.input and field.onChange to Checkbox.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.
  • Remember to add data-slot="checkbox-group" to the FieldGroup component for proper styling and spacing.

Radio group

  • Wire field.input and field.onChange to RadioGroup.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Switch

  • Wire field.input and field.onChange to Switch.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

NumberInput

  • Wire field.input and field.onChange to NumberInput.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Slider

  • Wire field.input and field.onChange to Slider.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Combobox

  • Wire field.input and field.onChange to Combobox.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Autocomplete

  • Wire field.input and field.onChange to Autocomplete.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Date Picker

  • Wire field.input and field.onChange to DatePicker.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Input OTP

  • Wire field.input and field.onChange to InputOTP.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Rating

  • Use field.onChange with Rating’s onValueChange and bind value from field.input.
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

File Upload

  • Use FileUpload onFileAccept to push files into the field value (field.onChange).
  • Add the invalid prop to the Field component and pass the error message to the FieldError component.

Complex Forms

Here is an example of a more complex form with multiple fields and validation.

Resetting the Form

Import reset and pass the form to reset the form to its default values.

import { reset } from "@formisch/react";

<Button
  type="button"
  variant="outline"
  onClick={() => reset(form)}
>
  Reset
</Button>

Array Fields

Formisch provides a FieldArray component for managing dynamic array fields. Also provides helpers like insert and remove for dynamic lists. This is useful when you need to add or remove fields dynamically.

Array Field Structure

Use FieldArray component and pass the form to the of prop.

form.tsx
import { FieldArray, Field as FormischField, useForm } from "@formisch/react";

export const ExampleForm = () => {
  const form = useForm({
    // ... form config
  });

  return (
    <FieldArray of={form} path={["emails"]}>
      {(arrayField) => (
        arrayField.items.map((itemId, index) => (
          // Nested field for each array item
        ))
      )}
    </FieldArray>
  );
}

Nested Fields

Use arrayField.items to render each nested field, passing the correct path for each item.

form.tsx
{
  arrayField.items.map((itemId, index) => (
    <FormischField 
      key={itemId} 
      of={form} 
      path={["emails", index, "contact", "address"]}
    >
      {(field) => (
        <Field invalid={Boolean(field.errors?.length)} orientation="horizontal">
          <FieldContent>
            <InputGroup>
              <InputGroupInput
                {...field.props}
                autoComplete="email"
                onChange={(e) => field.onChange(e.target.value)}
                placeholder="name@example.com"
                type="email"
                value={field.input}
              />
              {arrayField.items.length > 1 && (
                <InputGroupAddon align="inline-end">
                  <InputGroupButton
                    aria-label={`Remove email ${String(index + 1)}`}
                    onClick={() =>
                      remove(form, { path: ["emails"], at: index })
                    }
                    size="icon-xs"
                    type="button"
                    variant="ghost"
                  >
                    <XIcon aria-hidden className="size-4" />
                  </InputGroupButton>
                </InputGroupAddon>
              )}
            </InputGroup>
            <FieldError>{field.errors?.[0]}</FieldError>
          </FieldContent>
        </Field>
      )}
    </FormischField>
  ))
}

Adding items

  • Use insert to add a new array item.
  • Provide initialInput matching the nested structure of the array element.
form.tsx
import { insert } from "@formisch/react";

<Button
  type="button"
  variant="outline"
  size="sm"
  onClick={() =>
    insert(form, {
      path: ["emails"],
      initialInput: { contact: { address: "" } },
    })
  }
  disabled={arrayField.items.length >= 5}
>
  Add Email Address
</Button>

Removing items

Use remove with the array path and index.

form.tsx
import { remove } from "@formisch/react";

{
  arrayField.items.length > 1 && (
    <InputGroupAddon align="inline-end">
      <InputGroupButton
        type="button"
        variant="ghost"
        size="icon-xs"
        onClick={() => remove(form, { path: ["emails"], at: index })}
        aria-label={`Remove email ${index + 1}`}
      >
        <XIcon />
      </InputGroupButton>
    </InputGroupAddon>
  );
}

Array validation

Use Valibot's array method to validate array fields.

form.tsx
import * as v from "valibot";

const formSchema = v.object({
  emails: v.pipe(
    v.array(
      v.object({
        contact: v.object({
          address: v.pipe(
            v.string(),
            v.nonEmpty("Enter an email address."),
            v.email("Enter a valid email address.")
          ),
        }),
      })
    ),
    v.minLength(1, "Add at least one email address."),
    v.maxLength(5, "You can add up to 5 email addresses.")
  ),
});