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

# TextField

A text field is a native `<input>` or `<textarea>` with a label, a description
and an error message. It has [Field](/components/field/) built in for the labelling,
and adds the value, read-only mode, form reset, and hover and focus state on
every part. The browser keeps doing the editing: typing, selection, input
methods, autofill, undo, Enter submitting the form from an `<input>` and adding
a line in a `<textarea>`.

A native text control needs no WAI-ARIA pattern. Labelling follows the APG
practice on
[names and descriptions](https://www.w3.org/WAI/ARIA/apg/practices/names-and-descriptions/).

## Usage

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

export function EmailField(props: {
  error?: string;
  onEmailChange(email: string): void;
}) {
  const email = useBehavior(textField, {
    isRequired: true,
    isInvalid: props.error !== undefined,
    onValueChange: props.onEmailChange,
  });
  return (
    <div {...email.parts.root}>
      <label {...email.parts.label}>Email</label>
      <div {...email.parts.wrapper}>
        <input {...email.parts.input} name="email" type="email" />
      </div>
      <p {...email.parts.description}>We never share it</p>
      <p {...email.parts.error}>{props.error}</p>
    </div>
  );
}
```

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

const props = defineProps<{ error?: string }>();
const emit = defineEmits<{ emailChange: [email: string] }>();

const email = useBehavior(textField, () => ({
  isRequired: true,
  isInvalid: props.error !== undefined,
  onValueChange: (value) => emit('emailChange', value),
}));
</script>

<template>
  <div v-bind="email.parts.root">
    <label v-bind="email.parts.label">Email</label>
    <div v-bind="email.parts.wrapper">
      <input v-bind="email.parts.input" name="email" type="email" />
    </div>
    <p v-bind="email.parts.description">We never share it</p>
    <p v-bind="email.parts.error">{{ error }}</p>
  </div>
</template>
```

```html
<uni-text-field id="email" required>
  <div data-uni="text-field">
    <label data-element="Label">Email</label>
    <div data-element="Wrapper">
      <input data-element="Input" name="email" type="email" />
    </div>
    <p data-element="Description">We never share it</p>
    <p data-element="Error" hidden>Enter a valid email</p>
  </div>
</uni-text-field>

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

  document.querySelector('#email').addEventListener('valuechange', (event) => {
    console.log(event.detail);
  });
</script>
```

The element takes the input's current text as its default: the markup's
`value`, or what the user typed before the script loaded.

Without `value`, the field is uncontrolled: it keeps the typed text and
reports each change. `defaultValue` (the `default-value` attribute) sets the
text it starts with.

### Multiple lines

Render a `<textarea>` as the input part. Nothing else changes: there is no
separate TextArea component, and Enter adds a line instead of submitting.

```html
<uni-text-field>
  <label data-element="Label">Bio</label>
  <textarea data-element="Input" name="bio" rows="3" maxlength="160"></textarea>
</uni-text-field>
```

Native attributes such as `maxlength`, `pattern` or `rows` are yours and keep
their native effects. Growing a textarea with its content isn't built in.

### Prefixes and suffixes

The `wrapper` part is the box around the input, where icons, units, a prefix
or a clear button go. A press anywhere on it outside the input and outside any
other control (a link, a button, a label) focuses the input, so the whole box
behaves like the input. Hover and focus are written on every part, so the box
can show the hover and focus styles.

```html
<uni-text-field>
  <label data-element="Label">Website</label>
  <div data-element="Wrapper">
    <span aria-hidden="true">https://</span>
    <input data-element="Input" name="website" />
  </div>
</uni-text-field>
```

### Controlled value

Pass `value` and update it from `onValueChange`. You can change what the user
typed, for example to upper case:

```tsx
const [code, setCode] = useState('');
const coupon = useBehavior(textField, {
  value: code,
  onValueChange: (next) => setCode(next.toUpperCase()),
});
```

```vue
<script setup lang="ts">
const code = ref('');
const coupon = useBehavior(textField, () => ({
  value: code.value,
  onValueChange: (next) => (code.value = next.toUpperCase()),
}));
</script>

<template>
  <div v-bind="coupon.parts.root">
    <label v-bind="coupon.parts.label">Coupon</label>
    <input v-bind="coupon.parts.input" name="coupon" />
  </div>
</template>
```

```html
<script type="module">
  const coupon = document.querySelector('#coupon');
  coupon.value = ''; // setting the property makes it controlled
  coupon.addEventListener('valuechange', (event) => {
    coupon.value = event.detail.toUpperCase();
  });
</script>
```

A controlled field keeps the typed text on screen while you answer, so a store
that updates late loses no keystrokes and the caret stays put:

* A new `value` that echoes what the field reported leaves the input alone.
* Any other `value`, such as a transformed one or one set from elsewhere,
  replaces the text at once, and the caret moves to the end.
* Text you never accept goes back to your `value` when the input loses focus.
* While an input method is composing text, a new `value` waits until the
  composition ends.

### In a form

Put `name` on the input: the native input is what the form submits. On the
form's `reset`, the field goes back to its default, as a native input goes
back to its `value` attribute. An uncontrolled field follows `defaultValue`
again (empty without one), even one you set after it rendered, and reports
the change through `valueChange`. A controlled field reports `defaultValue`,
or without one the value it started with, and keeps your value until you
accept. A reset cancelled with `preventDefault()` before it reaches the form
changes nothing.

In a [Form](/components/form/), the form holds the value under the field's `name`,
checks it when focus leaves the input (`isRequired`, the constraints you
write on the input, such as `type="email"` or `minlength`, and `validate`)
and shows the first error in the `errorText` part. Without a form, set
`isInvalid` from your own checks, for example when `focusChange` reports
`false`, as the demo does. See [Forms](/guide/forms/).

## Anatomy

| Part          | Element               | Notes                                                                                                                                                                                                                                                                    |
| ------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `root`        | any; usually `div`    | Optional. The outermost element, around the other parts. No role.                                                                                                                                                                                                        |
| `wrapper`     | any; usually `div`    | Optional. The box around the input and your prefix or suffix. No role. A press on it outside any control focuses the input.                                                                                                                                              |
| `label`       | `label` by default    | As [Field](/components/field/)'s label.                                                                                                                                                                                                                                  |
| `input`       | `input` or `textarea` | UniAria writes Field's `id` (unless it has one), `aria-labelledby`, `aria-describedby`, `aria-invalid` and `aria-required`, plus `disabled`, `readonly` and the `value` property. You own `name`, `type`, `placeholder`, `autocomplete` and the other native attributes. |
| `description` | any                   | As Field's description.                                                                                                                                                                                                                                                  |
| `error`       | any                   | As Field's error: hidden until `isInvalid`.                                                                                                                                                                                                                              |
| `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.                                                                                                                                            |

A part keeps an `id` you give it, and everything that refers to the part
follows it; UniAria writes an `id` only on a part that has none (see
[Ids](/guide/accessibility/#ids)).

Every part but the input is optional. The parts link as
[Field's](/components/field/#anatomy) do. Set `disabled` and `readonly` through the
props, not on the input: UniAria writes those attributes and removes yours.

## Props

| Prop                     | Attribute                                                 | Type                 | Default   | Description                                                                                                                                  |
| ------------------------ | --------------------------------------------------------- | -------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `value` / `defaultValue` | `default-value`; the `value` property makes it controlled | `string`             | `''`      | The text. Changes come from `valueChange`.                                                                                                   |
| `isDisabled`             | `disabled`                                                | `boolean`            | `false`   | Native `disabled` on the input. Marks every part and hides the error.                                                                        |
| `isReadOnly`             | `readonly`                                                | `boolean`            | `false`   | Native `readonly`: the input stays focusable and selectable, but can't be edited.                                                            |
| `isRequired`             | `required`                                                | `boolean`            | `false`   | `aria-required="true"` on the input, never the native `required`.                                                                            |
| `isInvalid`              | `invalid`                                                 | `boolean`            | —         | `aria-invalid="true"` on the input. Shows, links and announces the error. Unset in a [Form](/components/form/), the form's errors decide it. |
| `inputId`                | —                                                         | `string`             | generated | The input's `id` while the input has none of its own.                                                                                        |
| `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 input's `aria-labelledby`, after the label's.                                                                               |
| `describedBy`            | `described-by`                                            | `string`             | —         | More ids for the input's `aria-describedby`, after the description and the error.                                                            |
| `tooltip`                | `tooltip` (the tooltip's id)                              | a Tooltip's instance | —         | A [Tooltip](/components/tooltip/) for the input. Without it, the element links the `<uni-tooltip>` it sits in directly.                      |

In a [Form](/components/form/), TextField also takes `name`, `form`, `validate`,
`validationTrigger`, `validationDelay`, `validationMessages`, `dependsOn`,
`isEqual` and `shouldPreserve`: see [Field props for the
form](/components/form/#field-props-for-the-form).

## Events

| Event         | Callback prop   | Detail    | When                                                                                                                                                 |
| ------------- | --------------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `valuechange` | `onValueChange` | `string`  | The user changed the text (every `input` event, input-method composition included), or a form reset asks for the default. Never for a value you set. |
| `focuschange` | `onFocusChange` | `boolean` | The input gained or lost focus. A good moment to validate.                                                                                           |

The custom element dispatches each event on itself, bubbling, with the value
in `detail`. In React, the input's `onFocus`, `onBlur`, `onInput`,
`onPointerEnter`, `onPointerLeave`, `onCompositionStart` and
`onCompositionEnd` come from UniAria, and passing your own replaces them: use
the events above instead. `onChange`, `onKeyDown` and the rest are yours. In
Vue, a listener of your own runs beside UniAria's.

## State

| Field            | Type      | Meaning                                                                                                     |
| ---------------- | --------- | ----------------------------------------------------------------------------------------------------------- |
| `value`          | `string`  | The current value. When controlled, your `value`, even while the input shows text you haven't accepted yet. |
| `isFocused`      | `boolean` | The input has focus.                                                                                        |
| `isFocusVisible` | `boolean` | The input has focus from a keyboard or a screen reader. Typing after a click doesn't set it.                |
| `isHovered`      | `boolean` | A mouse or pen is over the input (not the label or the wrapper's padding).                                  |
| `isDisabled`     | `boolean` | The `isDisabled` prop, or the form's.                                                                       |
| `isReadOnly`     | `boolean` | The `isReadOnly` prop, or the form's.                                                                       |
| `isRequired`     | `boolean` | The `isRequired` prop.                                                                                      |
| `isInvalid`      | `boolean` | The `isInvalid` prop, else in a form whether it holds errors.                                               |
| `isErrorShown`   | `boolean` | The error is shown: invalid and not disabled.                                                               |

Typing, hovering and focusing never render your component again. To render
something from state in React or Vue, select it (Vue gives a ref):

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

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

## Styling

| Attribute               | When                                                    |
| ----------------------- | ------------------------------------------------------- |
| `data-uni="text-field"` | On the root, always.                                    |
| `data-element="<Part>"` | On every other part, always.                            |
| `data-hovered`          | A mouse or pen is over the input.                       |
| `data-focused`          | The input has focus.                                    |
| `data-focus-visible`    | The input has focus from a keyboard or a screen reader. |
| `data-disabled`         | The field is disabled.                                  |
| `data-readonly`         | The field is read-only.                                 |
| `data-invalid`          | The field is invalid.                                   |
| `data-required`         | The field is required.                                  |

Every state attribute goes on every part, so the box, the label and the
messages can follow the input: draw the focus ring on the wrapper with
`[data-focused]`, color the label with `[data-invalid]`. The error part also
gets `hidden` while it isn't shown. TextField sets no inline styles.

### Default styles

The optional [`@uniaria/styles`](/guide/default-styles/) package styles
every part, as in the demo above, whose CSS tab holds only the page's layout.
The wrapper is the box, with the focus ring on any focus inside it and the
danger border while invalid; the input is bare inside it, beside your own prefix
or suffix. Without a wrapper the input has no box: use [Field](/components/field/)
around a native input for that.

## Keyboard

TextField handles no keys; the input keeps its native editing.

| Key    | Behavior                                                                            |
| ------ | ----------------------------------------------------------------------------------- |
| Enter  | In an `<input>`, submits its form, natively. In a `<textarea>`, adds a line.        |
| Escape | Not handled: it reaches your handlers and closes a dialog around the field.         |
| Tab    | Moves focus normally. A disabled input leaves the Tab order; a read-only one stays. |

## Accessibility

* **Spoken output.** A field labelled "Email" reads "textbox, Email" when
  empty, "textbox, Email, <jane@example.com>" with a value, "textbox, Email,
  <jane@example.com>, read only" while read-only and "textbox, Email, disabled"
  while disabled. A `<textarea>` is a textbox too; `type="search"` reads
  "searchbox". The description, the error and the required and invalid
  states follow, as in [Field](/components/field/#accessibility).
* **Labels.** A placeholder isn't a label: always render the label part, or
  pass `labelledBy`.
* **Errors.** Setting `isInvalid` shows the error and announces it, as Field
  does. Don't add `role="alert"` to the error part.
* **Read-only or disabled.** A read-only input stays in the Tab order and is
  read with its value. A disabled one leaves it, so prefer read-only for a
  value the user should still be able to find and copy.
* **Tooltips.** A [Tooltip](/components/tooltip/) linked through `tooltip` describes
  the input, and typing closes it.

## Compared with other libraries

* **Controlled typing is buffered.** React and React Aria put a refused value
  back right after the keystroke. TextField keeps the typed text until the
  input loses focus, so an echo that arrives late doesn't move the caret or
  drop keystrokes. A transformed value still replaces the text at once.
* **One component for both elements.** As in React Aria, a `<textarea>` is a
  TextField; there is no `elementType` to set.
* **The box focuses the input.** A press on the wrapper outside any control
  focuses the input. React Aria has no such part.
* **Validation is the form's.** In a [Form](/components/form/), `validate` and the
  native constraints you write on the input are checked, as React Aria's
  `validate` and native validation do; outside one, pass `isInvalid`.
* **Form reset** goes back to the `defaultValue` the field has when the form
  resets, as a native input does; React Aria keeps the one it started with. A
  controlled field without `defaultValue` goes back to the value it started
  with, as in React Aria.
