Skip to content
Wave 3

Form

A form validated by a Zod schema on submit, with errors shown next to each field.

  • A single search field: use Input.

Install

Terminal
pnpm dlx shadcn@latest add @acme/form

Or import from the package: import { Form } from '@acme/react'

Examples

Schema

You can add more later.

form-schema.tsx
'use client'

import { useState } from 'react'
import { z } from 'zod'
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
import { Button } from '@/components/ui/button'
import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field'
import { Form } from '@/components/ui/form'
import { Input } from '@/components/ui/input'

const schema = z.object({
  company: z.string().min(2, 'Enter your company name.'),
  email: z.email('Enter an email like name@company.com.'),
  seats: z.coerce
    .number('Enter a number.')
    .int()
    .min(1, 'At least 1 seat.')
    .max(500, 'Up to 500 seats; contact sales for more.'),
})

export default function FormSchema() {
  const [done, setDone] = useState<z.infer<typeof schema> | null>(null)
  return (
    <div className="flex w-full max-w-sm flex-col gap-4">
      <Form schema={schema} onSubmit={setDone}>
        <Field name="company">
          <FieldLabel>Company</FieldLabel>
          <Input autoComplete="organization" />
          <FieldError />
        </Field>
        <Field name="email">
          <FieldLabel>Billing email</FieldLabel>
          <Input type="email" autoComplete="email" />
          <FieldError />
        </Field>
        <Field name="seats">
          <FieldLabel>Seats</FieldLabel>
          <Input inputMode="numeric" defaultValue="5" />
          <FieldDescription>You can add more later.</FieldDescription>
          <FieldError />
        </Field>
        <div>
          <Button type="submit">Start trial</Button>
        </div>
      </Form>
      {done ? (
        <Alert variant="success">
          <AlertTitle>Trial started</AlertTitle>
          <AlertDescription>
            {done.company}, {done.seats} seats. We emailed {done.email}.
          </AlertDescription>
        </Alert>
      ) : null}
    </div>
  )
}

Props

Form props
PropTypeDefaultDescription
schema (required)ZodType—Validation schema; keys match each Field name.
onSubmit (required)(values) => void | Promise<void>—Called with parsed, typed values when the schema passes.
serverErrorsRecord<string, string | string[]>—Errors from the server, keyed by field name.

Accessibility

Keyboard
KeyAction
EnterSubmits from a text field
Role
form
You provide
  • A Field with name, FieldLabel and FieldError for each input.
  • A submit Button.
Built in
  • On a failed submit, focus moves to the first invalid field (WCAG 3.3.1).
  • Errors are linked to their inputs with aria-describedby and aria-invalid.
  • Native browser validation bubbles are off (noValidate); the schema is the single source of rules.
WCAG 2.2 criteria
3.3.1, 3.3.2, 3.3.3, 4.1.2

Don’t

  • Validate on every keystroke.
  • Clear what people typed when validation fails.

Tokens

  • color.destructive