# Field

Connect a control with its label, help and validation.

Project name with description

[Complete TSX file](/examples/field/default.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <Field name="project" className="w-full max-w-sm">
      <FieldLabel>Project name</FieldLabel>
      <Input />
      <FieldDescription>Visible to your team.</FieldDescription>
    </Field>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/input
```

With npm: `npx shadcn@4.18.0 add @siteplane/input`. [Registry JSON](/registry/input.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/action-icons @siteplane/button @siteplane/checkbox @siteplane/combobox @siteplane/input-group @siteplane/select @siteplane/textarea
```

## Usage

```tsx
import { Field, FieldLabel } from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

<Field>
  <FieldLabel>Name</FieldLabel>
  <Input />
</Field>;
```

## Composition

```text
Field
├── FieldLabel
├── Input or FieldControl
├── FieldDescription
├── FieldError
└── FieldValidity (optional)
```

A label or a description wraps a long unbroken value, such as a URL or a German compound, within the width of the field, so the field never grows past its container.

## Custom validation

Field owns validation timing and error association. Return null for a valid value or an error string; this example validates on blur.

Project slug validation

[Complete TSX file](/examples/field/validation.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";

export default function Example() {
  return (
    <Field
      className="w-full max-w-sm"
      name="slug"
      validationMode="onBlur"
      validate={(value) =>
        /^[a-z0-9-]+$/.test(String(value))
          ? null
          : "Use lowercase letters, numbers and hyphens."
      }
    >
      <FieldLabel>Project slug</FieldLabel>
      <Input defaultValue="Website Redesign" />
      <FieldDescription>Used in the project URL.</FieldDescription>
      <FieldError />
    </Field>
  );
}
```

FieldError animates by itself, with no code on your side. A message expands with a fade instead of pushing the form down, and when it clears it collapses while it still shows its last message. The space between the control and the error opens with it. While a message leaves it is hidden from assistive technology. Reduced motion shows and hides the message at once. FieldError uses the Motion component, which installs with the Field.

A native reset of the form around the Field, from a reset button or `form.reset()`, returns the Field to how it was before the first submit: its error collapses, and its touched and dirty marks clear. In a [Form](/docs/components/form/#controlled-reset), the fields validate again on the next submit.

## Floating label

Set `labelPlacement="floating"` and the label sits inside the field; it moves to the top on focus or once there is a value. It works with Input, Textarea, Select, a single-select Combobox and Autocomplete, and with trailing actions such as the show-password button of an Input Group. Floating fields take no leading icon, so every label and value starts on the same line; a field that needs a leading icon uses the top label. Floating fields have one size, 48 px.

Floating label fields

[Complete TSX file](/examples/field/floating-label.tsx)

```tsx
"use client";

import { useState } from "react";
import { RevealIcon } from "@/siteplane/components/action-icons";
import { Button } from "@/siteplane/components/button";
import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/siteplane/components/combobox";
import { Field, FieldLabel } from "@/siteplane/components/field";
import { Input } from "@/siteplane/components/input";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/siteplane/components/input-group";
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";
import { Textarea } from "@/siteplane/components/textarea";

const countries = [
  { label: "Germany", value: "de" },
  { label: "Austria", value: "at" },
  { label: "Switzerland", value: "ch" },
];

const timezones = [
  "Europe/Berlin",
  "Europe/London",
  "America/New_York",
  "Asia/Tokyo",
];

export default function Example() {
  const [visible, setVisible] = useState(false);

  return (
    <div className="grid w-full max-w-sm gap-4">
      <Field labelPlacement="floating">
        <FieldLabel>First name</FieldLabel>
        <Input autoComplete="given-name" />
      </Field>
      <Field labelPlacement="floating">
        <FieldLabel>Email</FieldLabel>
        <Input autoComplete="email" type="email" />
      </Field>
      <Field labelPlacement="floating">
        <FieldLabel>Password</FieldLabel>
        <InputGroup>
          <InputGroupInput
            autoComplete="new-password"
            type={visible ? "text" : "password"}
          />
          <InputGroupAddon align="inline-end">
            <Button
              aria-label="Show password"
              aria-pressed={visible}
              profile="field-action"
              shape="square"
              onClick={() => setVisible((current) => !current)}
            >
              <RevealIcon revealed={visible} />
            </Button>
          </InputGroupAddon>
        </InputGroup>
      </Field>
      <Field labelPlacement="floating">
        <FieldLabel>Country</FieldLabel>
        <Select items={countries}>
          <SelectTrigger className="w-full">
            <SelectValue />
          </SelectTrigger>
          <SelectPopup>
            {countries.map((country) => (
              <SelectItem key={country.value} value={country.value}>
                {country.label}
              </SelectItem>
            ))}
          </SelectPopup>
        </Select>
      </Field>
      <Field labelPlacement="floating">
        <FieldLabel>Timezone</FieldLabel>
        <Combobox items={timezones}>
          <ComboboxInput className="w-full" />
          <ComboboxPopup>
            <ComboboxEmpty>No timezones found.</ComboboxEmpty>
            <ComboboxList>
              {(timezone: string) => (
                <ComboboxItem key={timezone} value={timezone}>
                  {timezone}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxPopup>
        </Combobox>
      </Field>
      <Field labelPlacement="floating">
        <FieldLabel>Message</FieldLabel>
        <Textarea />
      </Field>
    </div>
  );
}
```

## Required fields

Set `required` on the control to make a field required. To mark it, add `requiredIndicator` to the Field: its label then shows a red asterisk while the control is required, above the field or inside a floating label. Whether a form marks its required fields is your choice per form. The control's own `required` stays the only source, so there is nothing to keep in sync, and screen readers hear the control's required state, not the asterisk.

To warn when a required field is left empty, set `validationMode="onBlur"` on the Field or the Form and add a FieldError for `valueMissing`. The message shows once the field is left empty and clears while the missing value is typed; a submit shows every message at once. The [contact details form](/docs/components/form/#floating-labels) puts both together.

```tsx
<Field requiredIndicator validationMode="onBlur">
  <FieldLabel>First name</FieldLabel>
  <Input required />
  <FieldError match="valueMissing">Enter your first name.</FieldError>
</Field>
```

## Error tooltip

In a floating Field, FieldError shows its message in a red bubble beside the control instead of below it, so nothing in the form moves when it appears; elsewhere it reveals the line below the field. Set `variant` to choose either way: `variant="tooltip"` beside a top label, or `variant="inline"` for the line below a floating field. The bubble shows its message while you are in the field; other invalid fields show the invalid frame, so one bubble shows at a time, like the browser's own validation bubble. A field left empty turns invalid, and its bubble shows when you return to it, until the value is valid. A submit with invalid fields moves focus to the first of them, which shows its bubble. The bubble points at the control from its end side, or from below on a narrow screen. It never takes the pointer or focus, and it stays the control's error description for screen readers. The [contact details form](/docs/components/form/#floating-labels) uses it.

```tsx
<Field labelPlacement="floating" requiredIndicator validationMode="onBlur">
  <FieldLabel>First name</FieldLabel>
  <Input required />
  <FieldError match="valueMissing">Enter your first name.</FieldError>
</Field>
```

## Select field

Field labels and describes a Select trigger. Pass `items` so the trigger shows the selected option's label.

Data region select

[Complete TSX file](/examples/field/with-select.tsx)

```tsx
"use client";

import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const regions = [
  { value: "eu-central", label: "EU Central" },
  { value: "us-east", label: "US East" },
  { value: "ap-south", label: "AP South" },
];

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Data region</FieldLabel>
      <Select items={regions} defaultValue="eu-central">
        <SelectTrigger className="w-full">
          <SelectValue />
        </SelectTrigger>
        <SelectPopup>
          {regions.map((region) => (
            <SelectItem key={region.value} value={region.value}>
              {region.label}
            </SelectItem>
          ))}
        </SelectPopup>
      </Select>
      <FieldDescription>Choose where your data is stored.</FieldDescription>
    </Field>
  );
}
```

## Checkbox field

Wrap a Checkbox in FieldLabel to label it; the FieldDescription is announced with it. The checkbox sits on the first line of the label, also when the label wraps, and the description and the error start under the label text. For the options of a checkbox or radio group, use FieldItem as shown on [Fieldset](/docs/components/fieldset/). An option with a description of its own puts the control beside a column of FieldLabel and FieldDescription, as on [Radio Group](/docs/components/radio-group/#with-description).

Product updates opt-in

[Complete TSX file](/examples/field/item.tsx)

```tsx
"use client";

import { Checkbox } from "@/siteplane/components/checkbox";
import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/siteplane/components/field";

export default function Example() {
  return (
    <Field>
      <FieldLabel>
        <Checkbox name="updates" />
        Email me about product updates
      </FieldLabel>
      <FieldDescription>
        A short summary of new features, sent once a month.
      </FieldDescription>
    </Field>
  );
}
```

## API reference

The styled parts add `labelPlacement` and `requiredIndicator` on Field and `variant` on FieldError; everything else comes from [Base UI Field](https://base-ui.com/react/components/field). The following contracts are central to integration:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| Field.labelPlacement | top \| floating | top | Draws the label above the control, or inside it, where it floats up on focus or once there is a value. |
| Field.requiredIndicator | boolean | false | Marks the label with a red asterisk while the control is required. |
| Field.name | string | unset | Field name used for submission and external form errors. |
| Field.disabled | boolean | false | Disables the field and its control. |
| Field.validationMode | onSubmit \| onBlur \| onChange | onSubmit / Form | When validation runs; a field override wins over Form. |
| Field.validate | (value, formValues) => error \| null \| Promise | unset | Custom validation; asynchronous validation does not block onSubmit submission. |
| Field.validationDebounceTime | number | 0 | Milliseconds between onChange validation callbacks. |
| Field.invalid / dirty / touched | boolean | uncontrolled | External state overrides. |
| Field.actionsRef | ref | unset | Exposes validate(). |
| FieldError.match | boolean \| validity key | foundation matching | true always shows children; otherwise match the validation error. |
| FieldError.variant | inline \| tooltip | tooltip in a floating Field, inline otherwise | inline reveals the message below the control; tooltip shows it in a red bubble beside the control while the field has focus, so nothing moves and other invalid fields show the invalid frame. |
`FieldLabel` labels the control; `FieldDescription` supplies associated help;
`FieldError` displays validation messages. `FieldItem` scopes a label/control
pair. `FieldControl` and `FieldValidity` are direct foundation exports;
`FieldPrimitive` exposes the namespace. FieldValidity children receive validity,
error, errors, value and initialValue. All styled parts accept string className
and foundation refs/render props; no styled callback-class evaluation is added.

FieldError renders for validity errors, Form `errors`, or when `match` is true.
For an application-controlled error, set `invalid` on Field and `match` on
FieldError.

The Field spaces its parts by `--field-gap` (8 px). FieldError reads the same
value, so to change the spacing set it on the Field, for example
`className="[--field-gap:calc(var(--spacing)*3)]"`, instead of using a `gap-*` utility.

[Read the installed source](/source/field).

## Usage notes

Field is installed with Input; it is not a separate registry item. Use a real server validation boundary for persisted data. An invalid style alone neither validates nor explains an error.

Make a field required with `required` on its control. To show that, use `requiredIndicator` rather than an asterisk in the label text: the Field draws it only while the control is required. Name what is missing in the FieldError, such as “Enter your first name.”

A floating field shows its error as a tooltip by default; any other field reveals the line below it. Use the tooltip beside a top label as well where a layout shift hurts, such as a compact form with several fields in a row; in a single column, the line below reads more calmly. The tooltip shows its message while you are in the field; other invalid fields show the invalid frame.

With a floating label:

- A placeholder shows only after focus, under the floated label.
- Floating fields take no leading icon, so every label and value starts on the same line. A field that needs a leading icon uses the top label: an Input Group that opens with an addon, an Input with `insetStart`, and a Combobox or Autocomplete with a `startAddon` keep the label above the control even in a floating Field. Trailing actions, such as show password, clear and the Select caret, are fine.
- Use the top label for text prefixes and suffixes such as `https://`, and for Number Field, a multi-select Combobox with chips, Date Field, Time Field and OTP Field. They keep the label above the control even in a floating Field.
- Descriptions stay below the field; errors show as a tooltip by default.
