---
language: "en"
title: "Field"
canonical: "https://uniaria.tenphi.me/components/field/"
source: "docs/components/field.mdx"
index: true
---

# Field

A field gives a form control its label, description and error message, and
links them with ids and ARIA, so a screen reader reads the label, the
description, the error and the required and invalid states when the control
gets focus. In a [Form](/components/form/), it also binds the control's value to the
form by its `name`. Field renders no control: its `control` part goes on
yours.

Every UniAria form component already has Field inside, with its own `label`,
`description` and `error` parts: a TextField, a RadioGroup or a Combobox
needs no Field around it (see [In UniAria components](#in-uniaria-components)).
Use Field itself for a control UniAria doesn't have:

* **A native control** UniAria has no component for: a `<select>`, or a date,
  color or file `<input>`.
* **A third-party widget**, such as a date picker or a rich text editor (see
  [A control that isn't native](#a-control-that-isnt-native)).
* **A control of your own.** Compose Field into its behavior, and it gets the
  labelling and the form binding (see
  [A field of your own](/guide/custom-components/#a-field-of-your-own)).

Field shows validation state. In a form, the form checks the field and gives
Field its errors; without one, your code decides when a field is invalid and
passes `isInvalid`. See [Forms](/guide/forms/) for the whole picture.

There is no WAI-ARIA pattern for a field. Field follows the APG practice on
[names and descriptions](https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/).

## Usage

```tsx
import { field } from '@uniaria/core/field';
import { useBehavior } from '@uniaria/react';

export function CountryField(props: { error?: string }) {
  const country = useBehavior(field, {
    name: 'country',
    isRequired: true,
    isInvalid: props.error !== undefined,
  });
  return (
    <div {...country.parts.root}>
      <label {...country.parts.label}>
        Country <span aria-hidden="true">*</span>
      </label>
      <select {...country.parts.control}>
        <option value="">Choose a country</option>
        <option value="de">Germany</option>
        <option value="fr">France</option>
      </select>
      <p {...country.parts.description}>We ship your order there.</p>
      <p {...country.parts.error}>{props.error}</p>
    </div>
  );
}
```

```vue
<script setup lang="ts">
import { field } from '@uniaria/core/field';
import { useBehavior } from '@uniaria/vue';

const props = defineProps<{ error?: string }>();

const country = useBehavior(field, () => ({
  name: 'country',
  isRequired: true,
  isInvalid: props.error !== undefined,
}));
</script>

<template>
  <div v-bind="country.parts.root">
    <label v-bind="country.parts.label">
      Country <span aria-hidden="true">*</span>
    </label>
    <select v-bind="country.parts.control">
      <option value="">Choose a country</option>
      <option value="de">Germany</option>
      <option value="fr">France</option>
    </select>
    <p v-bind="country.parts.description">We ship your order there.</p>
    <p v-bind="country.parts.error">{{ error }}</p>
  </div>
</template>
```

```html
<uni-field id="country-field" required>
  <label data-element="Label">Country <span aria-hidden="true">*</span></label>
  <select data-element="Control" id="country" name="country">
    <option value="">Choose a country</option>
    <option value="de">Germany</option>
    <option value="fr">France</option>
  </select>
  <p data-element="Description">We ship your order there.</p>
  <p data-element="Error" hidden>Choose a country.</p>
</uni-field>

<script type="module">
  import '@uniaria/elements/field';

  const country = document.querySelector('#country');
  country.addEventListener('change', () => {
    document
      .querySelector('#country-field')
      .toggleAttribute('invalid', country.value === '');
  });
</script>
```

`parts.control` holds what Field writes on your control: its `id` (unless it
has one), `aria-labelledby`, `aria-describedby`, `aria-invalid`,
`aria-required` and `name`, and listeners that follow its value and focus.
The rest of the control is yours: its options, `type`, `multiple`,
`disabled`.

Give the name to Field, not to the control. Field writes it on the control,
so a native form post sends it. In React and Vue, Field doesn't read the
control's attributes: a `name` written only on the `<select>` leaves the field
without one, and in a [Form](/components/form/) it doesn't bind. The custom element
is the exception: `<uni-field>` reads the name from its control's `name`
attribute.

Render the error part every time: Field hides it with `hidden` while the
field is valid.

### Showing errors

When `isInvalid` turns on, Field shows the error, links it to the control and
announces its text, as `role="alert"` would. While the error stays shown, new
text in it is announced too, so one error part can say "Enter your email" and
then "Enter a valid email". The timing is yours: validate on submit, on blur,
or as the user types.

An error already shown when the field starts isn't announced: the screen
reader reads it when the control gets focus. Nothing is announced while the
field is disabled, or for an error part that is missing or empty.

### In a form

In a [Form](/components/form/), leave `isInvalid` unset: the form checks the field
(`isRequired`, the native constraints on the control, then `validate`) and
gives Field its errors. Field follows the control's value, and an `errorText`
part inside the error shows the first error:

```tsx
const country = useBehavior(field, { name: 'country', isRequired: true });

<p {...country.parts.error}>
  <span {...country.parts.errorText} />
</p>;
```

The field binds to the form around it (through `OwnerProvider` in React), or
to the one in its `form` prop. While the form controls the value, Field writes
it back to the control, a `<select multiple>`'s options included. See
[Field props for the form](/components/form/#field-props-for-the-form).

### A control that isn't native

A widget that isn't a native control (a date picker from another library, a
rich text editor) needs two things from you:

* **The label as another element.** Only a native control can be labelled by
  a `<label>`. Render the label as a `<span>` and pass its tag as
  `labelElementType`. The label then gets no `for`, and names the control
  through `aria-labelledby` only. A click on it doesn't focus the control.
* **The value.** Field can't read a widget's value. Report each change with
  Field's `setValue` action, and render the value from Field's state: the
  form's while bound.

```tsx
import { field } from '@uniaria/core/field';
import { useBehavior, useBehaviorState } from '@uniaria/react';

export function DueDateField() {
  const due = useBehavior(field, {
    name: 'due',
    isRequired: true,
    labelElementType: 'span',
  });
  const value = useBehaviorState(due, (state) => state.value as Date | null);
  return (
    <div {...due.parts.root}>
      <span {...due.parts.label}>Due date</span>
      {/* It must pass the props to its focusable element. */}
      <DatePicker
        {...due.parts.control}
        value={value}
        onChange={(date) => due.actions.setValue(date)}
      />
      <p {...due.parts.error}>
        <span {...due.parts.errorText} />
      </p>
    </div>
  );
}
```

```vue
<script setup lang="ts">
import { field } from '@uniaria/core/field';
import { useBehavior, useBehaviorState } from '@uniaria/vue';

const due = useBehavior(field, {
  name: 'due',
  isRequired: true,
  labelElementType: 'span',
});
const value = useBehaviorState(due, (state) => state.value as Date | null);
</script>

<template>
  <div v-bind="due.parts.root">
    <span v-bind="due.parts.label">Due date</span>
    <!-- It must pass the attributes to its focusable element. -->
    <DatePicker
      v-bind="due.parts.control"
      :model-value="value"
      @update:model-value="(date) => due.actions.setValue(date)"
    />
    <p v-bind="due.parts.error">
      <span v-bind="due.parts.errorText" />
    </p>
  </div>
</template>
```

```html
<uni-field id="due-field" required>
  <span data-element="Label">Due date</span>
  <date-picker data-element="Control"></date-picker>
  <p data-element="Error"><span data-element="ErrorText"></span></p>
</uni-field>

<script type="module">
  import '@uniaria/elements/field';

  const due = document.querySelector('#due-field');
  due.querySelector('date-picker').addEventListener('change', (event) => {
    due.setValue(event.target.value);
  });
</script>
```

The element reads the label's tag, so there is no attribute for it. Its
`setValue()` method is the action, and its `value` property the value.

For radios and checkboxes, use [RadioGroup](/components/radio-group/) and
[CheckboxGroup](/components/checkbox-group/), which have Field built in.

### Extra labels and descriptions

`labelledBy` and `describedBy` add ids of other elements after Field's own,
for a hint that sits elsewhere on the page or a unit next to the control.
Don't write `aria-labelledby` or `aria-describedby` on the control yourself:
Field owns them and replaces them.

```html
<p id="password-rules">At least 12 characters.</p>
<uni-field described-by="password-rules">…</uni-field>
```

### In UniAria components

Every UniAria form component has Field built in: its label, description and
error are parts of the component itself, with the same props, the same
linking, the same error announcements and the same form binding. Render them
on the component, with no Field around it:

```tsx
const plan = useBehavior(radioGroup, { name: 'plan', isRequired: true });

<div {...plan.parts.root}>
  <span {...plan.parts.label}>Plan</span>
  <OwnerProvider owner={plan}>{/* the radios */}</OwnerProvider>
  <p {...plan.parts.description}>You can change it later.</p>
  <p {...plan.parts.error}>
    <span {...plan.parts.errorText} />
  </p>
</div>;

const fruit = useBehavior(combobox, { items: fruits, name: 'fruit' });

<div {...fruit.parts.root}>
  <label {...fruit.parts.label}>Fruit</label>
  <div {...fruit.parts.anchor}>{/* the input and its button */}</div>
  <p {...fruit.parts.description}>We deliver it fresh.</p>
  <p {...fruit.parts.error}>
    <span {...fruit.parts.errorText} />
  </p>
  {/* the popover with the list */}
</div>;
```

Field's `control` is the element the component's label names:

| Component                                    | Field's control | Label element by default | Notes                                                                                                                                 |
| -------------------------------------------- | --------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
| [TextField](/components/text-field/)         | `input`         | `label`                  | An `<input>` or a `<textarea>`.                                                                                                       |
| [NumberField](/components/number-field/)     | `input`         | `label`                  | The label also names the stepper buttons.                                                                                             |
| [Checkbox](/components/checkbox/)            | `input`         | `label`                  | Inside a `<label>` wrapper or root, the label part is a `<span>`. In a group, each checkbox also links the group's error while shown. |
| [Switch](/components/switch/)                | `input`         | `label`                  | As Checkbox.                                                                                                                          |
| [RadioGroup](/components/radio-group/)       | `root`          | `span`                   | The `radiogroup`. Each radio links its own label and description, and the group's error while shown.                                  |
| [CheckboxGroup](/components/checkbox-group/) | `root`          | `span`                   | The `group`.                                                                                                                          |
| [Listbox](/components/listbox/)              | `list`          | `span`                   |                                                                                                                                       |
| [Select](/components/select/)                | `trigger`       | `span`                   |                                                                                                                                       |
| [Combobox](/components/combobox/)            | `input`         | `label`                  |                                                                                                                                       |

The component's markers and state attributes go on those parts, so style
them through the component (`[data-uni='text-field']`), not through `field`. Where
you can name the control's id with a prop, the prop is named after the part:
`inputId`, `triggerId`, `listId`. An `id` you write on the part itself wins.

Use Field itself only for native and third-party controls. Don't wrap a
UniAria component in `<uni-field>`: the field doesn't look inside another
UniAria element, so its label would link to nothing, and in development it
warns. To build your own component on Field, see
[A field of your own](/guide/custom-components/#a-field-of-your-own).

## Anatomy

| Part          | Element                                                        | Notes                                                                                                                                                                                                                                                                                                                     |
| ------------- | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `root`        | any; usually `div`                                             | Optional. The outermost element, around the other parts, for styling. No role.                                                                                                                                                                                                                                            |
| `label`       | `label` by default; any element through `labelElementType`     | You own the content. UniAria writes its `id` (unless it has one), and `for` when it is a `label`.                                                                                                                                                                                                                         |
| `control`     | any control: `input`, `textarea`, `select`, or a custom widget | UniAria writes `id`, `aria-labelledby`, `aria-describedby`, `aria-invalid` and `aria-required`; `name` from the `name` prop, and `form` when it sits outside its form's `<form>`; a native control's value while a form or `value` controls it. You own the rest: `type`, `disabled`, `readonly`, a custom widget's role. |
| `description` | any                                                            | UniAria writes its `id` (unless it has one).                                                                                                                                                                                                                                                                              |
| `error`       | any                                                            | UniAria writes its `id` (unless it has one), and `hidden` while the error isn't shown. You own the content.                                                                                                                                                                                                               |
| `errorText`   | any, inside `error`                                            | Optional. In a [Form](/components/form/), UniAria writes the first error's text here; leave it out to render errors yourself.                                                                                                                                                                                             |

Every part is optional. Every part keeps an `id` (unless it has one) you give it, and the links
below use it; UniAria writes an `id` (unless it has one) only on a part that has none. Write it
on the element (after the part spread in React, after `v-bind` in Vue), or
name the control's with
`controlId`. Without either, UniAria generates one (see
[Ids](/guide/accessibility/#ids)).

The parts link like this:

* **Label.** The label gets an `id` (unless it has one), and `for` pointing at the control when it
  is a `<label>`, so a click on it focuses the control and a native control is
  named before JavaScript runs. The control gets `aria-labelledby` with the
  label's id, then `labelledBy`. While the label is rendered, it names the
  control, so an `aria-label` on the control is ignored.
* **Description and error.** The control's `aria-describedby` lists the
  description, then the error while it is shown, then `describedBy`. A hidden
  error is left out, since screen readers read hidden content a reference
  points at.
* **Only rendered parts.** An id goes into a reference only once its part is
  rendered, so no reference points at nothing. In React that happens right
  after the first commit, in Vue right after mount, both before paint: server HTML has every `id` and the
  label's `for`, and gains the references on hydration. The custom element
  links on upgrade, and again whenever a part is added or removed.

## Props

| Prop               | Attribute                     | Type      | Default   | Description                                                                                                                                                                          |
| ------------------ | ----------------------------- | --------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `isInvalid`        | `invalid`                     | `boolean` | —         | `aria-invalid="true"` on the control, removed when false (never `"false"`). Shows, links and announces the error. Unset in a [Form](/components/form/), the form's errors decide it. |
| `isRequired`       | `required`                    | `boolean` | `false`   | `aria-required="true"` on the control. Never the native `required`, which would start the browser's own validation.                                                                  |
| `isDisabled`       | `disabled`                    | `boolean` | `false`   | `data-disabled` on every part, and hides the error. It doesn't disable the control: disable a native control yourself.                                                               |
| `controlId`        | —                             | `string`  | generated | The control's `id` while the control has none of its own. Server HTML from React or Vue has it, so the label's `for` is right before hydration.                                      |
| `labelElementType` | — (read from the label's tag) | `string`  | `'label'` | The label's tag. `for` goes on a `label` only.                                                                                                                                       |
| `labelledBy`       | `labelled-by`                 | `string`  | —         | More ids for the control's `aria-labelledby`, space-separated, after the label's.                                                                                                    |
| `describedBy`      | `described-by`                | `string`  | —         | More ids for the control's `aria-describedby`, after the description and the error: a hint elsewhere on the page, for example.                                                       |
| `value`            | — (property only)             | `unknown` | —         | Bindable: what a native control holds, or what `setValue` gave a custom one. Seeded by `defaultValue`, reported as `valueChange`. Ignored while bound to a form.                     |

Field also takes the props every field takes for a form: `name`, written on
the control even outside a form, `form`, `validate`, `validationTrigger`,
`validationDelay`, `validationMessages`, `dependsOn`, `isEqual` and
`shouldPreserve`. See [Field props for the
form](/components/form/#field-props-for-the-form).

Field reports `valueChange` for the user's changes and handles no keys: the
control keeps its own.

## State

| Field          | Type                                                    | Meaning                                                               |
| -------------- | ------------------------------------------------------- | --------------------------------------------------------------------- |
| `isInvalid`    | `boolean`                                               | The `isInvalid` prop, else in a form whether it holds errors.         |
| `isRequired`   | `boolean`                                               | The `isRequired` prop.                                                |
| `isDisabled`   | `boolean`                                               | The `isDisabled` prop, or the form's.                                 |
| `isErrorShown` | `boolean`                                               | The error is shown: invalid and not disabled.                         |
| `value`        | `unknown`                                               | The value: the control's, or the form's under its `name` while bound. |
| `errors`       | `unknown[]`                                             | The form's errors for it; `[]` outside a form.                        |
| `status`       | `'unvalidated' \| 'validating' \| 'valid' \| 'invalid'` | Its last check in the form.                                           |
| `isTouched`    | `boolean`                                               | The user changed or left it, in a form.                               |
| `isDirty`      | `boolean`                                               | Its value differs from its default, in a form.                        |
| `isBound`      | `boolean`                                               | It is bound to a form.                                                |

Field has one action, `setValue(value)`: a user's change from a custom
control, which a form takes as typing in a native one. Around a native
`input`, `select` or `textarea`, Field follows the control's value itself.

You rarely need it: the state attributes below cover styling. In React or
Vue, select a value with `useBehaviorState` (Vue gives a ref), and the
component renders again only when it changes:

```ts
const isErrorShown = useBehaviorState(email, (state) => state.isErrorShown);
```

On the custom element, read `element.instance.getState()`. See
[State](/guide/state/).

## Styling

| Attribute          | On               | When                   |
| ------------------ | ---------------- | ---------------------- |
| `data-uni="field"` | root             | Always.                |
| `data-element`     | every other part | Always.                |
| `data-invalid`     | every part       | `isInvalid` is set.    |
| `data-required`    | every part       | `isRequired` is set.   |
| `data-disabled`    | every part       | `isDisabled` is set.   |
| `hidden`           | `error`          | The error isn't shown. |

The state attributes go on every part, so you can style the label and the
messages from the field's state without reading it in JavaScript. A required
mark, an "(optional)" note and a success state are yours to draw.

### Default styles

The optional [`@uniaria/styles`](/guide/default-styles/) package styles the
label, the description and the error, as in the demo above, whose CSS tab holds
only the page's layout. A native text control (an `input` you type in, a
`textarea` or a `select`) becomes the input box, with the focus ring and the
invalid border; any other control, such as a group, keeps its own look.

## Accessibility

* **Spoken output.** For a label "Email", a description "We never share it"
  and an error "Enter a valid email", a screen reader reads the control as
  "textbox, Email, We never share it, required" while valid, and "textbox,
  Email, We never share it Enter a valid email, invalid, required" while
  invalid. A checkbox control reads "checkbox, Accept terms, You must accept,
  not checked, invalid, required".
* **The required mark.** Hide a `*` in the label with `aria-hidden="true"`:
  the control already announces "required".
* **Announcements.** Field announces a new error through UniAria's live
  announcer, a visually hidden region it adds to the page. Don't add
  `role="alert"` or `aria-live` to the error part, or it is read twice.
* **Disabled.** `isDisabled` hides the error, since a disabled control can't
  be fixed. A native control you disable reads "disabled, invalid" without
  the error text.
* **Groups.** A `role="group"` control announces neither invalid nor required;
  ARIA supports both on `radiogroup` but not on `group`. There, the error text
  in `aria-describedby` is all a screen reader user hears.
* **Owned attributes.** Pass more ids through `labelledBy` and `describedBy`
  instead of writing `aria-labelledby` or `aria-describedby`. Ids are yours
  to set on any part.
  React and Vue warn in development when you pass an attribute UniAria writes.

## Compared with other libraries

* **React Aria** links errors the same way, through `aria-describedby`, but
  announces nothing when one appears. Field announces it, as the
  `role="alert"` messages of many design systems do. Lion makes the error a
  live region instead.
* **Validation is the form's.** In a [Form](/components/form/), a field checks
  `isRequired`, the native constraints on its control and `validate`, as
  React Aria's `validate` and native validation do. Outside one, pass
  `isInvalid` from your form library or your own checks.
* **No `aria-errormessage`.** VoiceOver and NVDA don't read it, so the error
  goes in `aria-describedby`, as React Aria, Lion and Spectrum Web Components
  do. The APG's spin button example uses `aria-errormessage`.
* **No native `required`.** Field writes `aria-required` only, so the browser
  shows no validation bubbles of its own.
