Skip to main content

Overview

Enter a valid email address
Select a role
Choose the user’s permission level.
The Form component provides composable wrappers — FormField, FormItem, FormLabel, FormDescription, FormMessage — that connect to React Hook Form and uniformly apply A’raf DS token-based styling to labels, helper text, and error messages.
Form is a thin integration layer, not a standalone form UI. It requires react-hook-form as a peer dependency and pairs with any A’raf DS input component (Input, Select, Checkbox, etc.).

Installation

npm install @araf-ds/core react-hook-form @hookform/resolvers zod

Usage

import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormDescription,
  FormMessage,
  FormControl,
} from "@araf-ds/core"
import { Input, Select, Button } from "@araf-ds/core"

const schema = z.object({
  name: z.string().min(2, "Name must be at least 2 characters"),
  email: z.string().email("Enter a valid email address"),
  role: z.string().min(1, "Select a role"),
})

export function ProfileForm() {
  const form = useForm({
    resolver: zodResolver(schema),
    defaultValues: { name: "", email: "", role: "" },
  })

  return (
    <Form form={form} onSubmit={form.handleSubmit((data) => console.log(data))}>
      <FormField
        control={form.control}
        name="name"
        render={({ field }) => (
          <FormItem>
            <FormLabel>Full name</FormLabel>
            <FormControl>
              <Input placeholder="Ahmad Dani" {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />

      <FormField
        control={form.control}
        name="email"
        render={({ field }) => (
          <FormItem>
            <FormLabel required>Email address</FormLabel>
            <FormControl>
              <Input type="email" placeholder="[email protected]" {...field} />
            </FormControl>
            <FormDescription>We'll never share your email.</FormDescription>
            <FormMessage />
          </FormItem>
        )}
      />

      <Button type="submit">Save profile</Button>
    </Form>
  )
}

Variants

Basic Form Field

A single field with label and validation message.
<FormField
  control={form.control}
  name="username"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Username</FormLabel>
      <FormControl>
        <Input placeholder="badrinteractive" {...field} />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

With Helper Text

Use FormDescription to add supporting guidance below the input.
Must be at least 8 characters and include a number.
<FormField
  control={form.control}
  name="password"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Password</FormLabel>
      <FormControl>
        <Input type="password" {...field} />
      </FormControl>
      <FormDescription>
        Must be at least 8 characters and include a number.
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

With Validation Error

FormMessage automatically renders the Zod/RHF error message in the error state.
Enter a valid email address
<FormField
  control={form.control}
  name="email"
  render={({ field }) => (
    <FormItem>
      <FormLabel required>Email address</FormLabel>
      <FormControl>
        <Input type="email" {...field} />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

Complete Form Example

Profile settings
Write a short introduction about yourself.
<Form form={form} onSubmit={form.handleSubmit(onSubmit)}>
  <div className="grid grid-cols-2 gap-3">
    <FormField name="firstName" render={...} />
    <FormField name="lastName" render={...} />
  </div>
  <FormField
    name="bio"
    render={({ field }) => (
      <FormItem>
        <FormLabel>Bio</FormLabel>
        <FormControl><Textarea rows={3} {...field} /></FormControl>
        <FormDescription>Write a short introduction.</FormDescription>
        <FormMessage />
      </FormItem>
    )}
  />
  <div className="flex justify-end gap-2">
    <Button variant="outline">Cancel</Button>
    <Button type="submit">Save changes</Button>
  </div>
</Form>

API Reference

Form

form
UseFormReturn
required
The React Hook Form instance returned by useForm(). Passed to the internal FormProvider.
onSubmit
(data: T) => void
required
Submit handler. Typically form.handleSubmit(handler).
className
string
Additional class names for the <form> element.

FormField

control
Control
required
The control object from useForm().
name
string
required
Field name matching the schema key.
render
({ field, fieldState }) => ReactNode
required
Render function receiving registered field props and validation state.

FormItem

Container for a single field — adds consistent vertical spacing between label, control, description, and message.

FormLabel

required
boolean
default:"false"
Appends a red asterisk (*) to indicate required fields.

FormDescription

Renders helper text below the input in #667085 muted color.

FormMessage

Automatically reads and displays the validation error for the current field from fieldState.error. Renders nothing when there is no error.

FormControl

Binds the ARIA attributes (aria-invalid, aria-describedby) between the input and FormMessage. Always wrap your input component with FormControl.

Accessibility

  • FormLabel renders a <label> element with a for attribute automatically linked to the input via FormControl
  • FormMessage uses role="alert" so screen readers announce validation errors immediately on submit
  • FormControl sets aria-invalid="true" on the input when a field error exists
  • FormDescription is linked to the input via aria-describedby for assistive technology
  • Required fields should be marked with both the required prop on FormLabel and the Zod .min(1) validation rule

Do’s & Don’ts

Do

  • Always wrap inputs with FormControl to get correct ARIA bindings
  • Use FormDescription for guidance the user needs before they fill the field
  • Use FormMessage for validation errors triggered after interaction
  • Keep labels short and descriptive — “Email address” not “Please enter your email”

Don't

  • Don’t use FormDescription for error messages — use FormMessage instead
  • Don’t skip FormLabel — placeholder text alone is not accessible
  • Don’t place FormMessage above the input — errors should appear below
  • Don’t use form.handleSubmit outside the onSubmit prop — let Form manage it