# FormField

> Lays out a control with its Label, description and error, and links them for screen readers ("Email, text field, Enter a valid email").

Component · forms. Registry item `form-field`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/form-field.json

## Installation

```sh
npx nativecn-cli@latest add form-field
```

Also installs: [theme](https://nativecn.dev/docs/theme/theme), [form-field-context](https://nativecn.dev/docs/primitives/form-field-context), [label](https://nativecn.dev/docs/components/label), [text](https://nativecn.dev/docs/components/text).

## Usage

### FormField demo

Email and password fields in a FocusChain, with a description and an error.

```tsx
import { useState } from "react";

import { FormField } from "@/components/form-field";
import { Input } from "@/components/input";
import { FocusChain } from "@/components/primitives/focus-chain";

export function FormFieldDemo() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState<string>();

  const submit = () => setError(email.includes("@") ? undefined : "Enter a valid email");

  return (
    <FocusChain onSubmit={submit}>
      <FormField label="Email" description="We never share it." error={error} required>
        <Input
          value={email}
          onChangeText={setEmail}
          placeholder="you@example.com"
          keyboardType="email-address"
          autoCapitalize="none"
          autoComplete="email"
        />
      </FormField>
      <FormField label="Password" required>
        <Input secureTextEntry autoComplete="current-password" />
      </FormField>
    </FocusChain>
  );
}
```

## Props

| Prop | Type and description |
|---|---|
| `label` | string: the visible label and the control's accessible name |
| `description` | string: helper text under the control, read as its hint |
| `error` | string: replaces the description, sets status error, announced once |
| `required` | boolean: * marker and "required" in the spoken name (default false) |
| `status` | 'error' \| 'success' (defaults to 'error' while error is set) |
| `disabled` | boolean: passed down to the control (default false) |
| `children` | the control: Input, Textarea, InputOTP, Checkbox, Switch, Slider |
| `style` | ViewStyle, layout only, merged last |

## Accessibility

- The label is the control's accessible name, with ", required" added when required.
- The error, or else the description, is the control's hint.
- The visible label, description and error are hidden from screen readers, so nothing is read twice.
- An error is announced once when it appears or its text changes.
- disabled passes down to the control, which is announced as disabled.

## Notes

Wrap each field of a form in FormField, and the fields in FocusChain. FormField is library-neutral: with react-hook-form, render it inside a Controller and pass fieldState.error?.message as error.

## Source

### {components}/form-field.tsx

```tsx
import type { Ref } from "react";
import { View, type StyleProp, type ViewProps, type ViewStyle } from "react-native";

import { Label } from "@/components/label";
import {
  FormFieldProvider,
  type FormFieldProps as FormFieldContextProps,
} from "@/components/primitives/form-field-context";
import { Text } from "@/components/text";
import { createStyles } from "@/theme";

export type FormFieldProps = FormFieldContextProps &
  Omit<ViewProps, "children" | "style"> & {
    /** Layout only, merged last onto the root. */
    style?: StyleProp<ViewStyle>;
    ref?: Ref<View>;
  };

const useStyles = createStyles((t) => ({
  root: { ...{ gap: t.spacing[2] } },
}));

// The label, description and error are read through the control (its name and hint), so their
// visible copies are hidden from screen readers to avoid reading them twice.
const hiddenFromScreenReaders = {
  "aria-hidden": true,
  importantForAccessibility: "no-hide-descendants",
} as const;

/**
 * Lays out a form control with its Label, description and error, and links them to it: the
 * control inside (Input, Textarea, ...) reads "Email, text field, Enter a valid email". The
 * error replaces the description, sets the `error` status and is announced once when it appears.
 * `status` and `disabled` pass down to the control. `style` is merged last.
 */
export function FormField({
  label,
  description,
  error,
  required = false,
  status,
  disabled = false,
  children,
  style,
  ...props
}: FormFieldProps) {
  const styles = useStyles();
  return (
    <View style={[styles.root, style]} {...props}>
      {label ? (
        <Label required={required} {...hiddenFromScreenReaders}>
          {label}
        </Label>
      ) : null}
      <FormFieldProvider
        label={label}
        description={description}
        error={error}
        required={required}
        status={status}
        disabled={disabled}
      >
        {children}
      </FormFieldProvider>
      {error ? (
        <Text variant="small" color="destructive" {...hiddenFromScreenReaders}>
          {error}
        </Text>
      ) : description ? (
        <Text variant="small" color="mutedForeground" {...hiddenFromScreenReaders}>
          {description}
        </Text>
      ) : null}
    </View>
  );
}
```
