# Field

> A React form field wrapper every control shares: label, hint, animated error and counter, fieldsets, and the box recipe all inputs use.

- Type: React component (Forms)
- Access: Free (the source is open to everyone)
- Version: 1.0.1
- Install: `npx wingo-ui@latest add field`
- Docs: https://wingo-ui.com/components/field

## Interaction

Click or tap a label and its control takes focus; a click on the box's padding, an icon or a suffix like USD also lands in the input without moving the caret. Tab moves from field to field and the whole box takes a soft ring; Enter in a field submits the form like a native one. Press Continue with a title under 10 characters and the error slides in under the field while the rest of the form glides down; type again and it slides away, and a hint swaps for the error with a crossfade instead of a jump. With a limit set, the counter turns amber at 90% and red at the limit, and screen readers hear how many characters are left. A read-only field keeps its text selectable but ignores typing, a disabled one dims with its label. Horizontal fields put the label beside the control once the field is 448px wide and stack it on top below that, so on a phone every label sits above its field and every input is 16px so iOS never zooms.

## Installation

```bash
npx wingo-ui@latest add field
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/field.json`

Files:

- `components/ui/field.tsx`

npm dependencies: `npm i @radix-ui/react-slot motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import type { ComponentProps } from "react"
import { FIELD_INPUT, Field, FieldAddon, FieldBox, FieldControl, useFieldControl } from "@/components/ui/field"

export function PriceField({ error }: { error?: string }) {
  return (
    <Field label="Price" description="The final price, tax included." error={error} required>
      <FieldBox>
        <FieldControl>
          <input name="price" inputMode="decimal" placeholder="0" autoComplete="off" className={FIELD_INPUT} />
        </FieldControl>
        <FieldAddon>USD</FieldAddon>
      </FieldBox>
    </Field>
  )
}

// your own control: one hook, and it works alone or inside a <Field>
export function PlainInput({ label, error, ...props }: ComponentProps<"input"> & { label?: string; error?: string }) {
  const field = useFieldControl({ label, error })
  return field.wrap(
    <FieldBox invalid={field.invalid} disabled={field.disabled}>
      <input {...props} {...field.controlProps} className={FIELD_INPUT} />
    </FieldBox>,
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `ReactNode` | `""` | The label, linked to the control with htmlFor (or aria-labelledby for groups). Leave it empty for a bare control. |
| `description?` | `ReactNode` | `""` | Helper text under the control, linked with aria-describedby. The error takes its place while it shows. |
| `error?` | `ReactNode` | `""` | Replaces the description with an alert icon and the text, slides in and marks the field invalid. "", false and null are no error; true marks it invalid without text (prefer invalid for that). |
| `required?` | `boolean` | `false` | Shows the required mark after the label (hidden from screen readers) and tells the control to set required. |
| `optional?` | `boolean` | `false` | Shows optionalLabel after the label, for forms that mark the few optional fields instead of the many required ones. |
| `optionalLabel?` | `string` | `"Optional"` | The word after an optional label. |
| `requiredIndicator?` | `ReactNode` | `"*"` | The mark after a required label. |
| `labelAction?` | `ReactNode` |  | A link or small button at the end of the label row, like Forgot your password?. It sits outside the <label> and gets a 44px hit area on touch. |
| `hideLabel?` | `boolean` | `false` | Keeps the label for screen readers only, e.g. a search field whose icon already says what it is. |
| `invalid?` | `boolean` | `false` | The red ring and aria-invalid without a message, for forms that show errors elsewhere. Passed to the control through context. |
| `disabled?` | `boolean` | `false` | Dims the label and, through context, disables the control. A disabled FieldGroup disables every field inside. |
| `readOnly?` | `boolean` | `false` | The value stays visible and selectable but cannot change; the box drops its hover tint. Passed to the control through context. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | Label size (13 / 14 / 15px) and the box height horizontal labels align to. Controls take their own size; the demo passes the same to every box. |
| `orientation?` | `"vertical" \| "horizontal"` | `"vertical"` | horizontal puts the label in a column beside the control once the field itself is 448px wide (a container query, not the viewport) and stacks below that. Inherits from a FieldGroup. |
| `labelWidth?` | `number` | `160` | Width of the label column in horizontal fields, in px. Inherits from a FieldGroup. |
| `count?` | `number` | `0` | The current length, for the counter. Controls with showCount pass it for you. In the playground 0 follows what you type; any other value previews a counter state. |
| `maxLength?` | `number` | `0` | Shows count / max at the end of the message row, amber from 90% and red at the limit. 0 hides the counter. The demo also sets it on the input. |
| `counterLabel?` | `(remaining: number) => string` | `` "(remaining) => `${remaining} characters left`" `` | What screen readers hear once 10% or less is left, 500ms after typing stops (negative past the limit). The visible counter is hidden from them. |
| `id?` | `string` |  | The control's id (not the wrapper's). Defaults to useId(); children read it from context. |
| `labelBehavior?` | `"native" \| "focus"` | `"native"` | focus: a label click focuses the control instead of clicking it, so a label never opens a picker. Button triggers set it through useFieldControl. The label carries it as data-behavior. |
| `labelAs?` | `"label" \| "span"` | `"label"` | span for group controls (radio group, checkbox group, rating, a two-thumb slider), which point at it with aria-labelledby (FieldControl adds it). Clicking it focuses the group's current stop. |
| `children?` | `ReactNode` |  | The control. Inside a Field, library controls read the id, flags and messages from context; do not give them their own label (the Field's wins, with a dev warning). |
| `classNames?` | `Partial<Record<"labelRow" \| "label" \| "required" \| "optional" \| "labelAction" \| "message" \| "description" \| "error" \| "counter", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Classes for the root. The rest of the props (style, data-*, aria-*) land there too. |

## Parts

### FieldGroup (component)

A real <fieldset> with a <legend>, so screen readers announce the section before its fields. legend, description, error, columns (1 | 2 | 3 | 4: one column on phones, 2 from sm, 3 and 4 from md; span with sm:col-span-2), variant ("plain" | "card"), orientation and labelWidth (inherited by every field inside), gap ("sm" | "md" | "lg"), disabled (the native attribute plus context), required and requiredIndicator (a mark after the legend), classNames (legend, description, error, content). Props: FieldGroupProps.

### FieldBox (component)

The box every text field and trigger uses: variant ("soft" | "outline" | "underline" | "ghost"), size (inherits the Field's), radius, multiline, invalid / disabled / readOnly (default to the Field's), state ("open" | "closed" for pickers), asChild (a trigger button or a native select), focusOnPointerDown (default true: a press on the padding or an addon focuses the input inside), data-slot. Props: FieldBoxProps.

### FieldControl (component)

Wraps any single child (a native input or select, a range, a third-party control) and puts the Field's id, aria-describedby, aria-invalid and aria-required on it, plus disabled and readOnly while the Field has them. The child's own props win. Props: SlotProps.

### FieldAddon (component)

Text addons like "https://" or "USD" in the muted addon look; elements render as given and bare icons turn muted. Takes span props.

### FieldAction (component)

The 28px trailing icon button (32px in lg): label (required, its aria-label and title), icon, size (defaults to the Field's), keepFocus (default true, the input keeps focus while it is pressed), plus every Button prop. 44px hit area on touch. Props: FieldActionProps.

### FieldLabel (component)

The label on its own: as ("label" | "span"), behavior ("native" | "focus"), size, required, optional, optionalLabel, requiredIndicator, disabled. Reads its target, size and flags from the nearest Field; every prop overrides. Props: FieldLabelProps.

### FieldDescription (component)

Helper text in the caption style, with the Field's description id so aria-describedby finds it. Takes p props.

### FieldError (component)

The error text with its alert icon (icon replaces it, null hides it), with the Field's error id. Props: ComponentProps<"p"> & { icon?: ReactNode }.

### FieldCounter (component)

The visible "count / max", hidden from screen readers, with data-state="ok | near | over" (amber from 90%, red at the limit). Props: ComponentProps<"span"> & { count?: number; max: number }.

### FieldMessage (component)

The animated row under a control: description or error (never both, crossfading) plus the counter, inside a polite live region. Opens and closes with the collapse preset and works outside a Field too, e.g. under a checkbox. Props: FieldMessageProps.

### useFieldControl (hook)

The hook every control calls once. Merges its props with a parent Field or FieldGroup and returns id, labelId, describedBy, invalid, disabled, required, readOnly, size (the control's, else the Field's), orientation, controlProps (for a native element), ariaProps (for anything else), isWrapped and wrap(control), which renders the Field around the control when it has its own label, description, error or counter. Signature: (options: FieldControlOptions) => FieldControlState.

### useFieldContext (hook)

The nearest Field's ids and flags (id, labelId, describedBy, invalid, disabled, required, readOnly, size, orientation), or null outside one. For custom controls. Signature: () => FieldContextValue | null.

### useFormReset (hook)

Calls onReset after the element's form is reset, once the form has put its fields back, so uncontrolled controls return to their defaultValue. Signature: (ref: RefObject<HTMLElement | null>, onReset: () => void) => void.

### fieldBoxClasses (function)

The box classes for any element (a trigger button, a custom wrapper). Unknown values fall back to soft / md / md. Signature: (options: { variant?, size?, radius?, multiline?, className? }) => string.

### FIELD_BOX (constant)

The base class recipe of the field box. Import it; never retype the classes.

### FIELD_VARIANTS (constant)

The box classes per variant (soft, outline, underline, ghost).

### FIELD_SIZES (constant)

The box height and padding classes per size.

### FIELD_MULTILINE_SIZES (constant)

The box padding classes per size for multiline controls (textareas).

### FIELD_RADII (constant)

The box radius classes per size and radius.

### FIELD_INPUT (constant)

The classes of the bare input inside the box.

### FIELD_ADDON (constant)

The muted addon classes (text like "https://" or "USD").

### FIELD_TRIGGER (constant)

The classes of a picker trigger button styled as a field.

### FIELD_CHEVRON (constant)

The classes of the trailing chevron on picker triggers.

### FIELD_HEIGHT_VAR (constant)

The CSS variable that carries the box height, for parts that line up with it.

## Examples

- **Validation on blur**: Leave the email field and its error slides in while the form glides down; type a capital letter in the username and the hint crossfades into the error.
- **Horizontal settings**: orientation on a card FieldGroup: labels move into a 136px column once a field is 448px wide and stack on top below that, as here in a narrow column or on a phone.
- **Two columns**: columns={2}: one column on phones, two from 640px; the street spans both. The state is a native select in FieldBox asChild.
- **Box variants and sizes**: soft, outline, underline and ghost at 32, 40 and 48px, each with an icon and a suffix.
- **Counter**: 12 / 70, 66 / 70 in amber from 90%, 70 / 70 in red at the limit; type to move between them. Screen readers hear the count for the last 10%.
- **Label action and optional**: A link at the end of the label row, a FieldAction that reveals the password, and an optional mark.
- **Any control, and a disabled group**: FieldControl wires ids and aria onto a native range; untick the checkbox and the disabled FieldGroup below comes back to life.
- **Hidden label**: hideLabel keeps the name for screen readers in a toolbar search with a ghost box.

## See also

- [Input](https://wingo-ui.com/components/input) ([Markdown](https://wingo-ui.com/components/input.md))
- [Textarea](https://wingo-ui.com/components/textarea) ([Markdown](https://wingo-ui.com/components/textarea.md))
- [Number Input](https://wingo-ui.com/components/number-input) ([Markdown](https://wingo-ui.com/components/number-input.md))
- [Checkbox](https://wingo-ui.com/components/checkbox) ([Markdown](https://wingo-ui.com/components/checkbox.md))
- [Radio Group](https://wingo-ui.com/components/radio-group) ([Markdown](https://wingo-ui.com/components/radio-group.md))
- [Switch](https://wingo-ui.com/components/switch) ([Markdown](https://wingo-ui.com/components/switch.md))

---

Canonical: https://wingo-ui.com/components/field
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
