Glrk UI

Form

A wrapper for Base UI Form with server-side error propagation, JS object submission via onFormSubmit, global validationMode, and imperative validate() via actionsRef.

Profile form
Full form — all wrapper types, client-side validation on submit
Role
Signup form
Signup — field-level validation, error clears on change

Installation

npx shadcn@latest add @glrk-ui/form

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into your project.

ui/form.tsx
'use client'

import { Form as FormPrimitive } from '@base-ui/react/form'

import { cn } from '@/lib/utils'

function Form({ className, ...props }: React.ComponentProps<typeof FormPrimitive>) {
  return (
    <FormPrimitive
      data-slot="form"
      className={cn('flex flex-col gap-4', className)}
      {...props}
    />
  )
}

export { Form }

Usage

Basic

Form renders a native <form>. Use it with Field and FieldNativeError for native validation, or with field-wrapper components for controlled validation.

import { Form } from "@/components/ui/form"
import { InputWrapper } from "@/components/ui/field-wrapper"

<Form onSubmit={handleSubmit} className="flex flex-col gap-4">
  <InputWrapper name="email" label="Email" type="email" />
  <button type="submit">Submit</button>
</Form>

Server-side errors

Pass a Record<fieldName, message> to errors. Errors propagate via context to Field.Root components with matching name and are displayed by FieldNativeError:

import { Form } from "@/components/ui/form"
import { Field, FieldLabel, FieldNativeError } from "@/components/ui/field"
import { Input } from "@/components/ui/input"

const [errors, setErrors] = useState<Record<string, string>>({})

async function handleSubmit(e: React.FormEvent) {
  e.preventDefault()
  const res = await submitForm()
  if (res.errors) setErrors(res.errors)
}

<Form errors={errors} onSubmit={handleSubmit}>
  <Field name="email">
    <FieldLabel>Email</FieldLabel>
    <Input name="email" type="email" onChange={() => setErrors({})} />
    <FieldNativeError />     {/* shows errors.email from Form context */}
  </Field>
</Form>

JS object submission (onFormSubmit)

onFormSubmit receives parsed form values as a plain object. preventDefault is called automatically:

<Form
  onFormSubmit={(values) => {
    console.log(values) // { email: "...", name: "..." }
  }}
>
  <Field name="email">
    <FieldLabel>Email</FieldLabel>
    <Input name="email" required />
    <FieldNativeError match="valueMissing">Required</FieldNativeError>
  </Field>
</Form>

Native validation

Use Field validate prop with FieldNativeError for custom validation. Set validationMode globally on Form:

<Form validationMode="onChange" onFormSubmit={handleSubmit}>
  <Field
    name="username"
    validate={(val) => {
      if (!val) return "Required"
      if ((val as string).length < 3) return "Min 3 characters"
      return null
    }}
  >
    <FieldLabel>Username</FieldLabel>
    <Input name="username" />
    <FieldNativeError />
  </Field>
</Form>

HTML constraint validation

<Form onFormSubmit={handleSubmit}>
  <Field name="email">
    <FieldLabel>Email</FieldLabel>
    <Input name="email" type="email" required />
    <FieldNativeError match="valueMissing">Email is required</FieldNativeError>
    <FieldNativeError match="typeMismatch">Enter a valid email</FieldNativeError>
  </Field>
  <Field name="password">
    <FieldLabel>Password</FieldLabel>
    <Input name="password" type="password" required minLength={8} />
    <FieldNativeError match="valueMissing">Required</FieldNativeError>
    <FieldNativeError match="tooShort">At least 8 characters</FieldNativeError>
  </Field>
</Form>

Imperative validation

Trigger validate() programmatically without submitting:

const actionsRef = useRef<{ validate: () => void } | null>(null)

<Form actionsRef={actionsRef} onFormSubmit={handleSubmit}>
  {/* fields */}
  <button type="button" onClick={() => actionsRef.current?.validate()}>
    Check
  </button>
  <button type="submit">Submit</button>
</Form>

Reference

Form

Prop

Type