Vesyl UI

Form

A complete form system built on React Hook Form with Zod validation. The Form component provides context for all form inputs, handles validation, and manages submission state.

Component structure

The Form system uses a compound component pattern:

ComponentDescription
FormComplete form wrapper (combines Root + Content)
Form.RootContext provider only (no DOM element)
Form.ContentThe <form> element
Form.FieldField wrapper with label, description, error display
Form.InputText input (used inside Form.Field)
Form.TextareaTextarea (used inside Form.Field)
Form.CheckboxCheckbox (used inside Form.Field)
Form.SwitchToggle switch (used inside Form.Field)
Form.RadioGroupRadio button group (used inside Form.Field)
Form.InputGroupInput with addons (icons, buttons, text)
Form.InputGroupInputText input inside InputGroup
Form.InputGroupAddonAddon container for icons/text
Form.DatePickerDate picker (used inside Form.Field)
Form.SubmitButtonSubmit button with automatic loading state

Basic usage

Use Form.Field to wrap form inputs with label, description, and error handling. The field passes its name and required state to child inputs via context.

<Form onSubmit={handleSubmit}>
  <Form.Field name="email" label="Email">
    <Form.Input />
  </Form.Field>
  <Form.Field name="password" label="Password">
    <Form.Input type="password" />
  </Form.Field>
  <Form.SubmitButton>Sign In</Form.SubmitButton>
</Form>

Form.Field props

PropTypeDefaultDescription
namestringField name (required)
labelReactNodeLabel text
descriptionReactNodeHelper text below input
requiredbooleanShow required indicator and validate
orientation'vertical' | 'horizontal''vertical'Layout direction
controlFirstbooleanPut input before label (for checkboxes)

For advanced layouts (card-style checkboxes, radio cards), see Field.

Form inputs

Form inputs connect to their parent Form.Field context automatically. Add validation with required on the Field.

USD

Horizontal layout

Use orientation="horizontal" to display label and input side by side. For switches and checkboxes, add controlFirst to place the control before the label.

We'll never share your email

The one you use every day

Receive updates via email

Validation with Zod

Pass a Zod schema to enable validation. Errors display automatically on the corresponding inputs.

import { z } from 'zod'

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Must be at least 8 characters'),
})

<Form schema={schema} onSubmit={handleSubmit}>
  <Form.Field name="email" label="Email">
    <Form.Input />
  </Form.Field>
  <Form.Field name="password" label="Password">
    <Form.Input type="password" />
  </Form.Field>
  <Form.SubmitButton>Sign In</Form.SubmitButton>
</Form>

Submit button outside form

When the submit button needs to be outside the <form> element (e.g., in a sticky action bar), use Form.Root and Form.Content separately. The submit button automatically connects via the HTML form attribute.

<Form.Root schema={schema} onSubmit={handleSubmit}>
  <Form.Content>
    <Form.Field name="email" label="Email">
      <Form.Input />
    </Form.Field>
    <Form.Field name="name" label="Name">
      <Form.Input />
    </Form.Field>
  </Form.Content>

  <ActionBar>
    <Form.SubmitButton>Save Changes</Form.SubmitButton>
  </ActionBar>
</Form.Root>
  • Form.Root — Provides form context (react-hook-form) without rendering a DOM element
  • Form.Content — Renders the <form> element with the connected id
  • Form.SubmitButton — Uses the form attribute to submit from anywhere within Form.Root

The standard Form component composes these internally:

<Form schema={schema} onSubmit={handleSubmit}>
  <Form.Field name="email" label="Email">
    <Form.Input />
  </Form.Field>
  <Form.SubmitButton>Save</Form.SubmitButton>
</Form>

Is equivalent to:

<Form.Root schema={schema} onSubmit={handleSubmit}>
  <Form.Content>
    <Form.Field name="email" label="Email">
      <Form.Input />
    </Form.Field>
    <Form.SubmitButton>Save</Form.SubmitButton>
  </Form.Content>
</Form.Root>

External form instance

For advanced control, create the form instance yourself and pass it to Form:

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'

const form = useForm({
  resolver: zodResolver(schema),
  defaultValues: { email: '' },
})

// Access form state
const isDirty = form.formState.isDirty

<Form form={form} onSubmit={handleSubmit}>
  <Form.Field name="email" label="Email">
    <Form.Input />
  </Form.Field>
  <Form.SubmitButton disabled={!isDirty}>Save</Form.SubmitButton>
</Form>