{
  "title": "Field",
  "description": "Layout and accessibility envelope that wires a label, description, error, and a form control together without managing form state.",
  "url": "/docs/components/field",
  "since": "0.1.0",
  "tags": [
    "form",
    "a11y",
    "layout"
  ],
  "platform": "both",
  "source": "---\ntitle: Field\ndescription: Layout and accessibility envelope that wires a label, description, error, and a form control together without managing form state.\nsince: 0.1.0\ntags: [form, a11y, layout]\nplatform: both\ncategory: inputs\n---\n\nimport { BundleSize } from '@/components/bundle-size';\nimport { Preview } from '@/components/preview';\nimport { PropsTable } from '@/components/props-table';\n\n<BundleSize component=\"Field\" />\n\n`<Field>` is a layout and accessibility envelope. It generates stable IDs, wires\n`aria-labelledby`, `aria-describedby`, `aria-invalid`, and `aria-required` onto\nthe wrapped control, and provides visual slots for a label, hint text, and an error\nmessage. It does not own form state — you bring your own state management and pass\n`error` down from wherever validation lives.\n\n`Field` has two API modes: **shorthand** (the common case) and **compound** (an\nescape hatch for custom layout). The shorthand API takes `label`, `description`,\nand `error` as props directly on `<Field>`, so the child is just the control.\nThe compound API uses explicit sub-components (`<Field.Label>`, `<Field.Description>`,\n`<Field.Control>`, `<Field.Error>`) for cases that need a non-standard slot layout.\n\n`Field` is form-framework agnostic. It works equally well with React Hook Form,\nZod + manual state, or no library at all. The RHF pattern using `<Controller>`\nis shown in the [react-hook-form](#react-hook-form-recommended) section.\n\n## Basic usage\n\nPass `label` and place the control as the child.\n\n<Preview name=\"field-basic\" />\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\n\nexport const Example = () => (\n    <Field label=\"Email\">\n        <TextInput placeholder=\"you@example.com\" />\n    </Field>\n);\n```\n\n`Field` injects `id`, `aria-labelledby`, and related ARIA attributes onto the\nchild control automatically. The child does not need to set them manually.\n\n## With description and error\n\nPass `description` and `error` as props:\n\n<Preview name=\"field-with-description-error\" />\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\nimport { useState } from 'react';\n\nexport default function UsernameField() {\n    const [value, setValue] = useState('');\n    const [error, setError] = useState<string | null>(null);\n\n    return (\n        <Field\n            label=\"Username\"\n            description=\"3 to 20 characters. Letters, numbers, and underscores only.\"\n            error={error}\n        >\n            <TextInput value={value} onChangeText={setValue} placeholder=\"your_username\" />\n        </Field>\n    );\n}\n```\n\nWhen `error` is set, `Field` renders the error message below the control and\nsets `aria-invalid` on the child. When both `description` and `error` are\npresent, `aria-describedby` contains both IDs — assistive technology reads both.\n\n## Required\n\nPass `required`:\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\n\nexport const Example = () => (\n    <Field label=\"Email\" required>\n        <TextInput placeholder=\"you@example.com\" />\n    </Field>\n);\n```\n\n`aria-required` is forwarded to the control. `Field` renders a visual indicator\n(a red asterisk by default) next to the label text, with a screen-reader-only\naccessible label. Both strings are configurable via the `NoriProvider` dictionary:\n\n| Key                      | Default | Purpose                                      |\n|--------------------------|---------|----------------------------------------------|\n| `field.requiredIndicator` | `*`    | Visual character appended to the label       |\n| `field.requiredLabel`     | `required` | Accessible label on the indicator span  |\n\n```tsx\nimport { NoriProvider } from '@nori-ui/core/client';\n\nexport const App = () => (\n    <NoriProvider dictionary={{ field: { requiredIndicator: '(required)', requiredLabel: 'required' } }}>\n        {/* ... */}\n    </NoriProvider>\n);\n```\n\n## Disabled\n\nPass `disabled`. The flag propagates to the child control and dims the label:\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\n\nexport const Example = () => (\n    <Field label=\"Email\" disabled>\n        <TextInput defaultValue=\"locked@example.com\" />\n    </Field>\n);\n```\n\nThe child component's own `disabled` prop is OR-ed with the field-level value,\nso you can also disable the control directly without going through the field.\n\n## Horizontal orientation\n\n`orientation=\"horizontal\"` renders the label and control side-by-side:\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\n\nexport const Example = () => (\n    <Field label=\"Name\" orientation=\"horizontal\">\n        <TextInput placeholder=\"Your name\" />\n    </Field>\n);\n```\n\nThe default is `\"vertical\"`.\n\n## Validating state\n\nWhile an async validation is in flight, pass `validating` to show a spinner and\nset `aria-busy`:\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\n\nexport const Example = () => (\n    <Field label=\"Username\" description=\"Checking availability…\" validating>\n        <TextInput defaultValue=\"claude\" />\n    </Field>\n);\n```\n\n`validating` and `error` can coexist — the spinner renders alongside the error\nmessage until the new result arrives.\n\n## Field.Group\n\n`Field.Group` is a fieldset-style envelope for grouped controls such as radio\nsets, checkbox groups, or multi-switch rows. It renders a `role=\"group\"` container\nlabelled by its own label — equivalent to `<fieldset>` + `<legend>` semantics,\nbut using `<View>` so layout is not constrained by browser fieldset quirks.\n\n<Preview name=\"field-group\" />\n\n```tsx\nimport { Field, Radio } from '@nori-ui/core';\nimport { useState } from 'react';\n\nexport default function PlanPicker() {\n    const [value, setValue] = useState<string | undefined>(undefined);\n    const error = value === undefined ? 'Please select a plan.' : null;\n    return (\n        <Field.Group label=\"Plan\" description=\"Pick the tier that fits your team.\" required error={error}>\n            <Radio.Group value={value} onChange={setValue} name=\"plan\">\n                <Radio value=\"hobby\" label=\"Hobby\" />\n                <Radio value=\"pro\" label=\"Pro\" />\n                <Radio value=\"enterprise\" label=\"Enterprise\" />\n            </Radio.Group>\n        </Field.Group>\n    );\n}\n```\n\n`Field.Group` accepts the same shorthand props as `Field` (`label`, `description`,\n`error`, `required`, `disabled`, `orientation`) except `name`. Pass `name`\ndirectly to the inner input group.\n\n## react-hook-form (recommended)\n\nUse `<Controller>` to connect Field to React Hook Form. Pass `field.value`,\n`field.onChange`, and `fieldState.error?.message` through:\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\nimport { useForm, Controller } from 'react-hook-form';\n\ntype FormValues = {\n    email: string;\n};\n\nexport default function SignUpForm() {\n    const { control, handleSubmit } = useForm<FormValues>({\n        defaultValues: { email: '' },\n    });\n\n    const onSubmit = (data: FormValues) => {\n        console.log(data);\n    };\n\n    return (\n        <form onSubmit={handleSubmit(onSubmit)}>\n            <Controller\n                control={control}\n                name=\"email\"\n                rules={{ required: 'Email is required.', pattern: { value: /\\S+@\\S+/, message: 'Not a valid email.' } }}\n                render={({ field, fieldState }) => (\n                    <Field\n                        label=\"Email\"\n                        required\n                        error={fieldState.error?.message ?? null}\n                    >\n                        <TextInput\n                            value={field.value}\n                            onChangeText={field.onChange}\n                            placeholder=\"you@example.com\"\n                        />\n                    </Field>\n                )}\n            />\n            <button type=\"submit\">Sign up</button>\n        </form>\n    );\n}\n```\n\nTo inject a server-side error after submission, call `setError`:\n\n```tsx\nimport { useForm } from 'react-hook-form';\n\nconst { setError } = useForm<FormValues>();\n\n// After a failed API call:\nsetError('email', { message: 'That email is already registered.' });\n```\n\n`react-hook-form` is not a peer dependency of `@nori-ui/core`. Install it\nseparately:\n\n```bash\nyarn add react-hook-form\n```\n\n## Manual / framework-agnostic\n\nField works without any form library. Use `useState` for local state and pass\na derived error string:\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\nimport { useState } from 'react';\n\nexport default function EmailField() {\n    const [value, setValue] = useState('');\n    const [touched, setTouched] = useState(false);\n\n    const error =\n        touched && value.length === 0\n            ? 'Email is required.'\n            : touched && !value.includes('@')\n              ? 'Not a valid email address.'\n              : null;\n\n    return (\n        <Field label=\"Email\" required error={error}>\n            <TextInput\n                value={value}\n                onChangeText={(v) => { setValue(v); setTouched(true); }}\n                placeholder=\"you@example.com\"\n            />\n        </Field>\n    );\n}\n```\n\n## Custom layout (compound API)\n\nWhen you need full control over slot placement — a control + button row, animated\nerror transitions, or a field with multiple controls — use the compound sub-components\ndirectly:\n\n```tsx\nimport { Field, TextInput } from '@nori-ui/core';\nimport { View } from 'react-native';\n\nexport const Example = () => (\n    <Field>\n        <Field.Label>Invite code</Field.Label>\n        <Field.Description>Check your welcome email for your code.</Field.Description>\n        <View style={{ flexDirection: 'row', gap: 8 }}>\n            <Field.Control>\n                <TextInput placeholder=\"XXXX-XXXX\" style={{ flex: 1 }} />\n            </Field.Control>\n            <button type=\"button\">Paste</button>\n        </View>\n        <Field.Error />\n    </Field>\n);\n```\n\nWhen to reach for the compound API:\n\n- The control sits inside a row alongside other elements (e.g., a button).\n- You need to animate the error message in/out independently.\n- You have multiple controls sharing one label (e.g., first + last name in a\n  single row) and need to manage the `id` wiring yourself.\n\n`Field.Control` clones its single child and injects `id`, `aria-labelledby`,\nand related ARIA attributes. Pass the `error` prop on `<Field>` even in compound\nmode — `Field.Control` reads it to set `aria-invalid`, and `Field.Error` reads\nit to render the message.\n\n## Standalone Label\n\nFor cases where `<Field>` is too much — standalone toggle rows or settings\nentries — use [`<Label>`](/docs/components/label) directly. It renders the same\nvisual style and required indicator as `Field.Label`, wired by an explicit\n`htmlFor` instead of auto-generated IDs.\n\n## API reference\n\n### `<Field>`\n\n<PropsTable component=\"Field\" />\n\n### `<Field.Label>`\n\nRenders a label text node wired to the field's generated ID. Clicking it moves\nfocus to the control. Used only in the [compound API](#custom-layout-compound-api).\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Label text. |\n\n### `<Field.Description>`\n\nRenders hint copy below the label. Its ID is wired to the control's\n`aria-describedby`. Used only in the [compound API](#custom-layout-compound-api).\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Hint text. |\n\n### `<Field.Error>`\n\nRenders the error message in the danger tone. Returns `null` when there is no\nerror and no children. Has `role=\"alert\"` on web. Used only in the\n[compound API](#custom-layout-compound-api) — in shorthand mode the error slot\nis rendered automatically when `error` is set.\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactNode` | — | Override the message from the `error` prop on `<Field>`. |\n\n### `<Field.Control>`\n\nClones its single child element and injects accessibility props. Used only in\nthe [compound API](#custom-layout-compound-api).\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `children` | `ReactElement` | — | A single form control element. |\n\nInjected props (your control receives these automatically):\n\n| Prop | Set when |\n|------|---------|\n| `id` | Always (uses `Field`'s generated ID) |\n| `aria-labelledby` | Always |\n| `aria-describedby` | `Field.Description` or `Field.Error` is present |\n| `aria-invalid` | `error` is truthy |\n| `aria-required` | `required` is true |\n| `disabled` | `disabled` is true on `Field` |\n| `name` | `name` prop on `Field` is set and child has no own `name` |\n\n### `<Field.Group>`\n\nFieldset-style group container for sets of related controls (radio buttons,\ncheckboxes, multi-switch rows). Accepts the same shorthand props as `<Field>`:\n\n| Prop | Type | Default | Description |\n|------|------|---------|-------------|\n| `label` | `string` | — | Visible group heading (rendered as `<legend>` equivalent). |\n| `description` | `string` | `null` | Hint text below the label. |\n| `error` | `string \\| null` | `null` | Validation error message. Sets `aria-invalid` on inner controls. |\n| `required` | `boolean` | `false` | Marks the group required. Appends visual indicator and sets `aria-required`. |\n| `disabled` | `boolean` | `false` | Disables all inner controls. |\n| `orientation` | `\"vertical\" \\| \"horizontal\"` | `\"vertical\"` | Stack direction for label and content. |\n| `children` | `ReactNode` | — | The grouped controls (e.g., `Radio.Group`, checkbox list). |\n"
}
