> ## Documentation Index
> Fetch the complete documentation index at: https://araf.badr.co.id/llms.txt
> Use this file to discover all available pages before exploring further.

# Form

> Composable form wrappers that connect to React Hook Form — providing consistent label, helper text, error message, and field spacing across all form elements.

## Overview

<div style={{padding:"28px 24px",background:"#f9fafb",borderRadius:"12px",border:"1px solid #e5e7eb",display:"flex",justifyContent:"center"}}>
  <div style={{display:"flex",flexDirection:"column",gap:"16px",width:"100%",maxWidth:"400px",fontFamily:"Inter,sans-serif"}}>
    <div style={{display:"flex",flexDirection:"column",gap:"4px"}}>
      <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Full name</label>

      <input readOnly value="Ahmad Dani" style={{border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none",width:"100%",boxSizing:"border-box"}} />
    </div>

    <div style={{display:"flex",flexDirection:"column",gap:"4px"}}>
      <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Email address <span style={{color:"#DC2626"}}>\*</span></label>

      <input readOnly value="" placeholder="you@example.com" style={{border:"1px solid #DC2626",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none",width:"100%",boxSizing:"border-box",boxShadow:"0 0 0 4px #FEE2E2"}} />

      <span style={{fontSize:"12px",color:"#DC2626"}}>Enter a valid email address</span>
    </div>

    <div style={{display:"flex",flexDirection:"column",gap:"4px"}}>
      <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Role</label>

      <div style={{border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#667085",background:"#fff",display:"flex",justifyContent:"space-between",alignItems:"center"}}>
        <span>Select a role</span>

        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#667085" strokeWidth="2">
          <polyline points="6 9 12 15 18 9" />
        </svg>
      </div>

      <span style={{fontSize:"12px",color:"#667085"}}>Choose the user's permission level.</span>
    </div>

    <button style={{background:"#0479CE",color:"#fff",border:"none",borderRadius:"8px",padding:"10px 16px",fontSize:"14px",fontWeight:500,cursor:"pointer",alignSelf:"flex-start"}}>Save profile</button>
  </div>
</div>

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.

<Note>
  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.).
</Note>

***

## Installation

<CodeGroup>
  ```bash npm theme={null}
  npm install @araf-ds/core react-hook-form @hookform/resolvers zod
  ```

  ```bash yarn theme={null}
  yarn add @araf-ds/core react-hook-form @hookform/resolvers zod
  ```

  ```bash pnpm theme={null}
  pnpm add @araf-ds/core react-hook-form @hookform/resolvers zod
  ```
</CodeGroup>

***

## Usage

```tsx theme={null}
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="you@example.com" {...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.

<div style={{padding:"24px",background:"#f9fafb",borderRadius:"12px",border:"1px solid #e5e7eb"}}>
  <div style={{display:"flex",flexDirection:"column",gap:"4px",maxWidth:"360px",fontFamily:"Inter,sans-serif"}}>
    <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Username</label>

    <input readOnly value="badrinteractive" style={{border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none"}} />
  </div>
</div>

```tsx theme={null}
<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.

<div style={{padding:"24px",background:"#f9fafb",borderRadius:"12px",border:"1px solid #e5e7eb"}}>
  <div style={{display:"flex",flexDirection:"column",gap:"4px",maxWidth:"360px",fontFamily:"Inter,sans-serif"}}>
    <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Password</label>

    <input readOnly type="password" value="••••••••" style={{border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none"}} />

    <span style={{fontSize:"12px",color:"#667085"}}>Must be at least 8 characters and include a number.</span>
  </div>
</div>

```tsx theme={null}
<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.

<div style={{padding:"24px",background:"#f9fafb",borderRadius:"12px",border:"1px solid #e5e7eb"}}>
  <div style={{display:"flex",flexDirection:"column",gap:"4px",maxWidth:"360px",fontFamily:"Inter,sans-serif"}}>
    <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Email address <span style={{color:"#DC2626"}}>\*</span></label>

    <input readOnly value="not-an-email" style={{border:"1px solid #DC2626",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none",boxShadow:"0 0 0 4px #FEE2E2"}} />

    <span style={{fontSize:"12px",color:"#DC2626"}}>Enter a valid email address</span>
  </div>
</div>

```tsx theme={null}
<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

<div style={{padding:"24px",background:"#f9fafb",borderRadius:"12px",border:"1px solid #e5e7eb"}}>
  <div style={{display:"flex",flexDirection:"column",gap:"16px",maxWidth:"400px",fontFamily:"Inter,sans-serif"}}>
    <div style={{fontSize:"16px",fontWeight:600,color:"#101828"}}>Profile settings</div>

    <div style={{display:"flex",gap:"12px"}}>
      <div style={{display:"flex",flexDirection:"column",gap:"4px",flex:1}}>
        <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>First name</label>

        <input readOnly value="Ahmad" style={{border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none",width:"100%",boxSizing:"border-box"}} />
      </div>

      <div style={{display:"flex",flexDirection:"column",gap:"4px",flex:1}}>
        <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Last name</label>

        <input readOnly value="Dani" style={{border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none",width:"100%",boxSizing:"border-box"}} />
      </div>
    </div>

    <div style={{display:"flex",flexDirection:"column",gap:"4px"}}>
      <label style={{fontSize:"12px",fontWeight:500,color:"#344054"}}>Bio</label>
      <textarea readOnly rows={3} style={{border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 12px",fontSize:"14px",color:"#101828",background:"#fff",outline:"none",resize:"none",fontFamily:"Inter,sans-serif"}}>Product designer based in Jakarta.</textarea>
      <span style={{fontSize:"12px",color:"#667085"}}>Write a short introduction about yourself.</span>
    </div>

    <div style={{display:"flex",gap:"8px",justifyContent:"flex-end"}}>
      <button style={{background:"#fff",color:"#344054",border:"1px solid #D0D5DD",borderRadius:"8px",padding:"9px 14px",fontSize:"14px",fontWeight:500,cursor:"pointer"}}>Cancel</button>
      <button style={{background:"#0479CE",color:"#fff",border:"none",borderRadius:"8px",padding:"9px 14px",fontSize:"14px",fontWeight:500,cursor:"pointer"}}>Save changes</button>
    </div>
  </div>
</div>

```tsx theme={null}
<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

<ParamField path="form" type="UseFormReturn" required>
  The React Hook Form instance returned by `useForm()`. Passed to the internal `FormProvider`.
</ParamField>

<ParamField path="onSubmit" type="(data: T) => void" required>
  Submit handler. Typically `form.handleSubmit(handler)`.
</ParamField>

<ParamField path="className" type="string">
  Additional class names for the `<form>` element.
</ParamField>

### FormField

<ParamField path="control" type="Control" required>
  The `control` object from `useForm()`.
</ParamField>

<ParamField path="name" type="string" required>
  Field name matching the schema key.
</ParamField>

<ParamField path="render" type="({ field, fieldState }) => ReactNode" required>
  Render function receiving registered field props and validation state.
</ParamField>

### FormItem

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

### FormLabel

<ParamField path="required" type="boolean" default="false">
  Appends a red asterisk (`*`) to indicate required fields.
</ParamField>

### 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

<CardGroup cols={2}>
  <Card title="Do" icon="check" iconType="solid" color="#16A34A">
    * 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"
  </Card>

  <Card title="Don't" icon="xmark" iconType="solid" color="#DC2626">
    * 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
  </Card>
</CardGroup>
