- Accordion
- Action BarUpdated
- Alert Dialog
- Alert
- Announcement
- Aspect Ratio
- Autocomplete
- Avatar
- BadgeUpdated
- Bottom Navigation
- Breadcrumb
- Button Group
- Button
- CalendarUpdated
- CardUpdated
- Carousel
- Chart
- Checkbox
- Circular Progress
- Circular Slider
- Clipboard
- Collapsible
- Color Picker
- Combobox
- Command
- Context MenuUpdated
- Data List
- Date Picker
- DialogUpdated
- DrawerUpdated
- Editable
- FieldUpdated
- File Upload
- Float
- Floating Panel
- Frame
- Hint
- Hover Card
- Image Cropper
- Input Group
- Input OTP
- Input
- Item
- Kbd
- Link Overlay
- Listbox
- MarqueeUpdated
- Menu
- Native Select
- Number InputUpdated
- Pagination
- Popover
- Progress
- Prose
- QR Code
- Radio Group
- Rating
- Resizable
- Scroll Area
- Segment Group
- Select
- Separator
- Sheet
- Sidebar
- Signature Pad
- Skeleton
- Skip Nav
- Slider
- Spinner
- Status
- Steps
- Switch
- TableUpdated
- Tabs
- Textarea
- TimerUpdated
- ToastUpdated
- Toggle Group
- Toggle Tooltip
- Toggle
- Tooltip
- Tour
- Tree View
This guide will cover building forms using the Field component, adding schema validation with Valibot, 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.
For the purposes of this demo, browser validation is disabled to illustrate schema validation. In production, keep native validation enabled when appropriate.
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
useFormhook for form state management. - Uses the
Formcomponent for submit handling. - Import Formisch’s
Fieldunder an alias (FormischField) for controlled inputs. - Uses Shark
Fieldcomponents 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>Bug Title</FieldLabel> <Input {...field.props} 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.
Note: Formisch only supports Valibot for validation.
import * as v from "valibot";
export const bugReportSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "Bug title must be at least 5 characters."),
v.maxLength(32, "Bug title must be at most 32 characters.")
),
description: v.pipe(
v.string(),
v.minLength(20, "Description must be at least 20 characters."),
v.maxLength(100, "Description must be at most 100 characters.")
),
});Setup#
- Create the form with
useFormfrom Formisch and pass your schema to theschemaoption, - Wrap fields in
Formand pass the form to theonSubmitoption.
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 Shark Field.
"use client";
import {
Form,
Field as FormischField,
reset,
type SubmitHandler,
useForm,
} from "@formisch/react";
import { toast } from "@registry/react/components/toast";
import * as v from "valibot";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardFooter,
CardHeader,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
InputGroup,
InputGroupAddon,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group";
const formSchema = v.object({
title: v.pipe(
v.string(),
v.minLength(5, "Bug title must be at least 5 characters."),
v.maxLength(32, "Bug title must be at most 32 characters.")
),
description: v.pipe(
v.string(),
v.minLength(20, "Description must be at least 20 characters."),
v.maxLength(100, "Description must be at most 100 characters.")
),
});
export const BugReportForm = () => {
const form = useForm({
schema: formSchema,
initialInput: {
title: "",
description: "",
},
});
const onSubmit: SubmitHandler<typeof formSchema> = (output) => {
toast.info({
id: "bug-report-submitted",
title: "Bug submitted",
description: (
<pre className="mt-2">
<code>{JSON.stringify(output, null, 2)}</code>
</pre>
),
});
};
return (
<Card asChild className="w-full sm:max-w-md">
<Form of={form} onSubmit={onSubmit}>
<CardHeader
description="Help us improve by reporting bugs you encounter."
title="Bug Report"
/>
<CardContent>
<FieldGroup>
<FormischField of={form} path={["title"]}>
{(field) => (
<Field invalid={Boolean(field.errors?.length)}>
<FieldLabel>Bug Title</FieldLabel>
<Input
{...field.props}
autoComplete="off"
placeholder="Login button not working on mobile"
value={field.input}
/>
<FieldError>{field.errors?.[0]}</FieldError>
</Field>
)}
</FormischField>
<FormischField of={form} path={["description"]}>
{(field) => (
<Field invalid={Boolean(field.errors?.length)}>
<FieldLabel>Description</FieldLabel>
<InputGroup>
<InputGroupTextarea
{...field.props}
className="min-h-24 resize-none"
placeholder="I'm having an issue with the login button on mobile."
rows={6}
value={field.input}
/>
<InputGroupAddon align="block-end">
<InputGroupText className="tabular-nums">
{field.input?.length}/100 characters
</InputGroupText>
</InputGroupAddon>
</InputGroup>
<FieldDescription>
Include steps to reproduce, expected behavior, and what
actually happened.
</FieldDescription>
<FieldError>{field.errors?.[0]}</FieldError>
</Field>
)}
</FormischField>
</FieldGroup>
</CardContent>
<CardFooter>
<Button onClick={() => reset(form)} variant="outline">
Reset
</Button>
<Button type="submit">Submit</Button>
</CardFooter>
</Form>
</Card>
);
};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.
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:
const form = useForm({ schema: formSchema, validate: "submit", revalidate: "input", })
| Option | Role |
|---|---|
"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
invalidprop to theFieldcomponent. - Don't need to add the
invalidprop to the form control such asInput,Checkbox, etc.
<FormischField of={form} path={["email"]}> {(field) => ( <Field invalid={Boolean(field.errors?.length)}> <FieldLabel>Email</FieldLabel> <Input {...field.props} type="email" value={field.input} /> <FieldError>{field.errors?.[0]}</FieldError> </Field> )} </FormischField>
Different types of fields#
Input#
- Spread
field.propsonInputand setvalue={field.input}. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Textarea#
- Spread
field.propsonTextareaand setvalue={field.input}. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
NativeSelect#
- Spread
field.propsonNativeSelectand setvalue={field.input}. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Select#
- Wire
field.inputandfield.onChangetoSelect. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Checkbox#
- Wire
field.inputandfield.onChangetoCheckbox. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent. - Remember to add
data-slot="checkbox-group"to theFieldGroupcomponent for proper styling and spacing.
Radio group#
- Wire
field.inputandfield.onChangetoRadioGroup. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Switch#
- Wire
field.inputandfield.onChangetoSwitch. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
NumberInput#
- Wire
field.inputandfield.onChangetoNumberInput. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Slider#
- Wire
field.inputandfield.onChangetoSlider. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Combobox#
- Wire
field.inputandfield.onChangetoCombobox. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Autocomplete#
- Wire
field.inputandfield.onChangetoAutocomplete. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Date Picker#
- Wire
field.inputandfield.onChangetoDatePicker. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Input OTP#
- Wire
field.inputandfield.onChangetoInputOTP. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
Rating#
- Use
field.onChangewithRating’sonValueChangeand bindvaluefromfield.input. - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
File Upload#
- Use
FileUploadonFileAcceptto push files into the field value (field.onChange). - Add the
invalidprop to theFieldcomponent and pass the error message to theFieldErrorcomponent.
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.
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.
{ 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
insertto add a new array item. - Provide
initialInputmatching the nested structure of the array element.
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.
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.
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.") ), });
On This Page