shadcn-solid
DocsComponentsCharts


Get Started
  • Introduction
  • Installation
  • Theming
  • Dark Mode
Form
  • Tanstack Form
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Badge
  • Breadcrumbs
  • Button Group
  • Button
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Data Table
  • Date Picker
  • Dialog
  • Drawer
  • Dropdown Menu
  • File Field
  • Hover Card
  • Kbd
  • Menubar
  • Navigation Menu
  • Number Field
  • OTP Field
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Resizable
  • Search
  • Segmented Control
  • Select
  • Separator
  • Sidebar
  • Skeleton
  • Slider
  • Sonner
  • Switch
  • Table
  • Tabs
  • Text Field
  • Toggle Group
  • Toggle Button
  • Tooltip

TanStack Form

Build forms in Solid using TanStack Form and Valibot.

Docs

This guide explores how to build forms using TanStack Form. You'll learn to create forms with the <TextField /> component, implement schema validation with Valibot, handle errors, and ensure accessibility.

Demo

We'll start by building the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors.

Note: For the purpose of this demo, we have intentionally disabled browser validation to show how schema validation and form errors work in TanStack Form. It is recommended to add basic browser validation in your production code.

Component tanstack-form-demo not found in registry.

Approach

This form leverages TanStack Form for powerful, headless form handling. We'll build our form using the <TextField /> component, which gives you complete flexibility over the markup and styling.

  • Uses TanStack Form's createForm hook for form state management.
  • form.Field component with render prop pattern for controlled inputs.
  • <Field /> components for building accessible forms.
  • Client-side validation using Valibot.
  • Real-time validation feedback.

Anatomy

Here's a basic example of a form using TanStack Form with the <TextField /> component.

<form
  onSubmit={(e) => {
    e.preventDefault()
    form.handleSubmit()
  }}
>
  <form.Field name="title">
    {(field) => (
      <TextField
        validationState={
          field().state.meta.isTouched && !field().state.meta.isValid
            ? "invalid"
            : "valid"
        }
        name={field().name}
        value={field().state.value}
        onBlur={field().handleBlur}
        onChange={field().handleChange}
      >
        <TextFieldLabel>Bug Title</TextFieldLabel>
        <TextFieldInput
          placeholder="Login button not working on mobile"
          autocomplete="off"
        />
        <TextFieldDescription>
          Provide a concise title for your bug report.
        </TextFieldDescription>
        <TextFieldErrorMessage errors={field().state.meta.errors} />
      </TextField>
    )}
  </form.Field>
  <button type="submit">Login</button>
</form>

Form

Create a schema

We'll start by defining the shape of our form using a Valibot schema.

form.tsx
import * as v from "valibot"
 
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."),
  ),
})

Setup the form

Use the createForm hook from TanStack Form to create your form instance with Valibot validation.

form.tsx
import { createForm } from "@tanstack/solid-form"
import { toast } from "somoto"
import * as v from "valibot"
 
const formSchema = v.object({
  // ...
})
 
export function BugReportForm() {
  const form = useForm({
    defaultValues: {
      title: "",
      description: "",
    },
    validators: {
      onSubmit: formSchema,
    },
    onSubmit: async ({ value }) => {
      toast.success("Form submitted successfully")
    },
  })
 
  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        form.handleSubmit()
      }}
    >
      {/* ... */}
    </form>
  )
}

We are using onSubmit to validate the form data here. TanStack Form supports other validation modes, which you can read about in the documentation.

Build the form

We can now build the form using the form.Field component from TanStack Form and the <TextField /> component.


form
import { createForm } from "@tanstack/solid-form"
import { toast } from "somoto"
import * as v from "valibot"
 
import { Button } from "@/registry/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/registry/ui/card"
import {
  TextField,
  TextFieldDescription,
  TextFieldErrorMessage,
  TextFieldInput,
  TextFieldLabel,
  TextFieldTextArea,
} from "@/registry/ui/text-field"
 
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."),
  ),
})
 
type formSchemaType = v.InferInput<typeof formSchema>
 
const TanstackFormDemo = () => {
  const form = createForm(() => ({
    defaultValues: {
      title: "",
      description: "",
    } as formSchemaType,
    validators: {
      onSubmit: formSchema,
    },
    onSubmit: (props) => {
      toast("You submitted the following values:", {
        description: (
          <pre class="bg-code text-code-foreground mt-2 w-[320px] overflow-x-auto rounded-md p-4">
            <code>{JSON.stringify(props.value, null, 2)}</code>
          </pre>
        ),
        position: "bottom-right",
        classNames: {
          content: "flex flex-col gap-2",
        },
        style: {
          "--border-radius": "calc(var(--radius)  + 4px)",
        },
      })
    },
  }))
 
  return (
    <Card class="w-full sm:max-w-md">
      <CardHeader>
        <CardTitle>Bug Report</CardTitle>
        <CardDescription>
          Help us improve by reporting bugs you encounter.
        </CardDescription>
      </CardHeader>
      <CardContent>
        <form
          id="bug-report-form"
          onSubmit={(e) => {
            e.preventDefault()
            void form.handleSubmit()
          }}
          class="flex w-full flex-col gap-7"
        >
          <form.Field name="title">
            {(field) => (
              <TextField
                validationState={
                  field().state.meta.isTouched && !field().state.meta.isValid
                    ? "invalid"
                    : "valid"
                }
                name={field().name}
                value={field().state.value}
                onBlur={field().handleBlur}
                onChange={field().handleChange}
              >
                <TextFieldLabel>Bug Title</TextFieldLabel>
                <TextFieldInput
                  placeholder="Login button not working on mobile"
                  autocomplete="off"
                />
                <TextFieldErrorMessage errors={field().state.meta.errors} />
              </TextField>
            )}
          </form.Field>
          <form.Field name="description">
            {(field) => (
              <TextField
                validationState={
                  field().state.meta.isTouched && !field().state.meta.isValid
                    ? "invalid"
                    : "valid"
                }
                name={field().name}
                value={field().state.value}
                onBlur={field().handleBlur}
                onChange={field().handleChange}
              >
                <TextFieldLabel>Description</TextFieldLabel>
                <TextFieldTextArea
                  placeholder="I'm having an issue with the login button on mobile."
                  rows={6}
                  class="min-h-24 resize-none"
                />
                <TextFieldDescription>
                  Include steps to reproduce, expected behavior, and what
                  actually happened.
                </TextFieldDescription>
                <TextFieldErrorMessage errors={field().state.meta.errors} />
              </TextField>
            )}
          </form.Field>
        </form>
      </CardContent>
      <CardFooter>
        <div class="flex gap-2">
          <Button
            type="button"
            variant="outline"
            onClick={() => {
              form.reset()
            }}
          >
            Reset
          </Button>
          <Button type="submit" form="bug-report-form">
            Submit
          </Button>
        </div>
      </CardFooter>
    </Card>
  )
}
 
export default TanstackFormDemo

Done

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

When you submit the form, the onSubmit function will be called with the validated form data. If the form data is invalid, TanStack Form will display the errors next to each field.

Working with Different Field Types

Input

  • For input fields, use field().state.value and field().handleChange on the <TextField /> component.
  • To show errors, pass field().state.meta.errors to the validationState prop of the <FieldField /> component.

Component tanstack-form-input-demo not found in registry.

Text Area

  • For text area fields, use field().state.value and field().handleChange on the <TextField /> component.
  • To show errors, pass field().state.meta.errors to the validationState prop of the <FieldField /> component.

Component tanstack-form-textarea-demo not found in registry.

Select

  • For select component, use field().state.value and field().handleChange on the <Select /> component.
  • To show errors, pass field().state.meta.errors to the validationState prop of the <Select /> component.

Component tanstack-form-select-demo not found in registry.

Checkbox

  • For checkbox component, use field().state.value and field().handleChange on the <Checkbox /> component.
  • To show errors, pass field().state.meta.errors to the validationState prop of the <Checkbox /> component.

Component tanstack-form-checkbox-demo not found in registry.

Radio Group

  • For radio group component, use field().state.value and field().handleChange on the <RadioGroup /> component.
  • To show errors, pass field().state.meta.errors to the validationState prop of the <RadioGroup /> component.

Component tanstack-form-radio-group-demo not found in registry.

Switch

  • For switch component, use field().state.value and field().handleChange on the <Switch /> component.
  • To show errors, pass field().state.meta.errors to the validationState prop of the <Switch /> component.

Component tanstack-form-switch-demo not found in registry.

Resetting the Form

Use form.reset() to reset the form to its default values.

<Button type="button" variant="outline" onClick={() => form.reset()}>
  Reset
</Button>
Dark ModeAccordion
Built & designed by shadcn. Ported to Solid by hngngn. The source code is available on GitHub.