# Form (/ui/components/react/form)



## Usage [#usage]

```tsx
import { Form } from '@appica/ui-react/form'
import { Field, FieldLabel, FieldError } from '@appica/ui-react/field'
```

```tsx
<Form onFormSubmit={(values) => console.log(values)}>
  <Field name="email">
    <FieldLabel>Email</FieldLabel>
    <Input type="email" placeholder="you@example.com" />
    <FieldError />
  </Field>
  <Button type="submit">Subscribe</Button>
</Form>
```

`Form` renders a native `<form>` and ties together the [`Field`](/ui/components/react/field)s inside it. On submit it validates every field, focuses the first invalid one, and only calls `onFormSubmit` — with the collected `{ name: value }` map — once they all pass. `preventDefault()` is handled for you when you use `onFormSubmit`.

It also closes the loop on server-side validation: pass an `errors` object keyed by field `name` and each matching `Field` shows the message through its `FieldError`, no extra wiring. Use `validationMode` to choose when fields validate (`onSubmit`, `onBlur`, or `onChange`); a `validationMode` set on an individual `Field` wins over the form's.

## Examples [#examples]

### Default [#default]

Give each `Field` a `name` and the values arrive in `onFormSubmit` as an object keyed by those names. The form handles `preventDefault` itself.

```tsx
'use client'

import { useState } from 'react'
import { Form } from '@appica/ui-react/form'
import { Field, FieldLabel } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
import { Button } from '@appica/ui-react/button'

export default function FormDefault() {
  const [submitted, setSubmitted] = useState<string | null>(null)

  return (
    <Form
      className="flex w-full max-w-70 flex-col gap-4"
      onFormSubmit={(values) => setSubmitted(String(values.email ?? ''))}
    >
      <Field name="name">
        <FieldLabel>Full name</FieldLabel>
        <Input placeholder="John Doe" />
      </Field>
      <Field name="email">
        <FieldLabel>Email</FieldLabel>
        <Input type="email" placeholder="you@example.com" />
      </Field>
      <Button type="submit">Subscribe</Button>
      {submitted ? <p className="text-foreground-muted text-sm">Subscribed as {submitted || 'no email'}.</p> : null}
    </Form>
  )
}
```

### Validation [#validation]

Validate on submit and reject the attempt when something's off. Here the handler builds an error map, marks each offending `Field` `invalid`, and shakes just those fields with the `animate-shake` utility so the ones needing attention nudge into view — the valid fields and the button stay put. Once a field has been flagged, it re-validates on every keystroke using the same rules — so the message clears the moment the value becomes valid (and updates if it's still wrong), instead of lingering until the next submit. `noValidate` hands validation to your code instead of the browser's native bubbles.

```tsx
'use client'

import { useState } from 'react'
import { Form } from '@appica/ui-react/form'
import { Field, FieldLabel, FieldError } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
import { Button } from '@appica/ui-react/button'

type FormErrors = { email?: string | undefined; password?: string | undefined }
type ShakeState = { email: boolean; password: boolean }

function validateEmail(value: string): string | undefined {
  const email = value.trim()
  if (!email) return 'Email is required'
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) return 'Enter a valid email address'
  return undefined
}

function validatePassword(value: string): string | undefined {
  if (value.length < 8) return 'Use at least 8 characters'
  return undefined
}

export default function FormValidation() {
  const [errors, setErrors] = useState<FormErrors>({})
  const [shake, setShake] = useState<ShakeState>({ email: false, password: false })

  const revalidate = (field: keyof FormErrors, value: string) =>
    setErrors((prev) => {
      if (!prev[field]) return prev
      const message = field === 'email' ? validateEmail(value) : validatePassword(value)
      return { ...prev, [field]: message }
    })

  return (
    <Form
      noValidate
      className="flex w-full max-w-70 flex-col gap-4"
      onSubmit={(event) => {
        event.preventDefault()
        const data = new FormData(event.currentTarget)
        const next: FormErrors = {
          email: validateEmail(String(data.get('email') ?? '')),
          password: validatePassword(String(data.get('password') ?? '')),
        }
        setErrors(next)
        setShake({ email: Boolean(next.email), password: Boolean(next.password) })
      }}
    >
      <Field
        name="email"
        invalid={Boolean(errors.email)}
        className={shake.email ? 'animate-shake' : undefined}
        onAnimationEnd={() => setShake((prev) => ({ ...prev, email: false }))}
      >
        <FieldLabel>Email</FieldLabel>
        <Input
          type="email"
          autoComplete="off"
          placeholder="you@example.com"
          onChange={(event) => revalidate('email', event.target.value)}
        />
        <FieldError match={Boolean(errors.email)}>{errors.email}</FieldError>
      </Field>
      <Field
        name="password"
        invalid={Boolean(errors.password)}
        className={shake.password ? 'animate-shake' : undefined}
        onAnimationEnd={() => setShake((prev) => ({ ...prev, password: false }))}
      >
        <FieldLabel>Password</FieldLabel>
        <Input
          type="password"
          autoComplete="new-password"
          placeholder="••••••••"
          onChange={(event) => revalidate('password', event.target.value)}
        />
        <FieldError match={Boolean(errors.password)}>{errors.password}</FieldError>
      </Field>
      <Button type="submit">Create account</Button>
    </Form>
  )
}
```

### Server-side errors [#server-side-errors]

When validation lives on the server, pass its response to the `errors` prop keyed by field `name`. The matching `Field` renders the message through its `FieldError`, and the error clears as the user edits the field. (Submit `taken@example.com` to see it.)

```tsx
'use client'

import { useState } from 'react'
import { Form } from '@appica/ui-react/form'
import { Field, FieldLabel, FieldDescription, FieldError } from '@appica/ui-react/field'
import { Input } from '@appica/ui-react/input'
import { Button } from '@appica/ui-react/button'

const TAKEN = ['taken@example.com']

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

  return (
    <Form
      errors={errors}
      className="flex w-full max-w-70 flex-col gap-4"
      onFormSubmit={(values) => {
        const email = String(values.email ?? '').trim()
        setErrors(TAKEN.includes(email) ? { email: 'This email is already registered' } : {})
      }}
    >
      <Field name="email">
        <FieldLabel>Email</FieldLabel>
        <Input type="email" placeholder="you@example.com" />
        <FieldDescription>Try taken@example.com to see a server error.</FieldDescription>
        <FieldError />
      </Field>
      <Button type="submit">Sign up</Button>
    </Form>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, popup placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

Labels, controls, and messages all align to the start edge and follow the resolved direction. For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="form" />

## API reference [#api-reference]

`Form` wraps [Base UI's Form](https://base-ui.com/react/components/form) and renders a native `<form>`, so every standard form attribute (`onSubmit`, `noValidate`, `action`, `method`, `ref`, …) passes straight through alongside the props below.

| Prop             | <ColMinWidth width="200">Type</ColMinWidth>                   | Default      | <ColMinWidth width="220">Description</ColMinWidth>                                                       |
| ---------------- | ------------------------------------------------------------- | ------------ | -------------------------------------------------------------------------------------------------------- |
| `onFormSubmit`   | <Code>(formValues, eventDetails) => void</Code>               | —            | Called on a valid submit with the collected `{ name: value }` map. `preventDefault()` is called for you. |
| `errors`         | <Code>Record\<string, string \| string\[]></Code>             | —            | External (typically server) errors keyed by field `name`; distributed to each `Field`'s `FieldError`.    |
| `validationMode` | <Code>'onSubmit' \| 'onBlur' \| 'onChange'</Code>             | `'onSubmit'` | When fields validate. A `Field`'s own `validationMode` takes precedence.                                 |
| `actionsRef`     | <Code>RefObject\<Form.Actions></Code>                         | —            | Imperative ref whose `validate(name?)` re-runs validation for all fields, or one field by `name`.        |
| `render`         | <Code>ReactElement \| ((props, state) => ReactElement)</Code> | —            | Replace the `<form>` element, or compose it with another component.                                      |
| `className`      | <Code>string \| ((state) => string)</Code>                    | —            | Extra classes, merged via `tailwind-merge`. May be a function of state.                                  |

## Accessibility [#accessibility]

* Renders a native `<form>`, so browser submit semantics (Enter to submit, `<button type="submit">`) work as expected. Name it with `aria-label` / `aria-labelledby` if it isn't already introduced by a heading.
* On an invalid submit the first invalid field receives focus, so keyboard and screen-reader users land on the problem.
* Validation messages come from each [`Field`](/ui/components/react/field)'s `FieldError`, which is wired to its control for assistive tech — see the Field page for the per-control accessibility details.
* The `animate-shake` feedback in the validation example is purely visual; it conveys nothing on its own, so the error message remains the source of truth.
