Siteplane UI

Field

Connect a control with its label, help and validation.

Markdown

Visible to your team.

"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

pnpm dlx shadcn@4.18.0 add @siteplane/input

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Usage

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

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

Composition

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.

Used in the project URL.

"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, 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.

"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 puts both together.

<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 uses it.

<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.

Choose where your data is stored.

"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. An option with a description of its own puts the control beside a column of FieldLabel and FieldDescription, as on Radio Group.

A short summary of new features, sent once a month.

"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. The following contracts are central to integration:

Component properties
PropTypeDefaultDescription
Field.labelPlacementtop | floatingtopDraws the label above the control, or inside it, where it floats up on focus or once there is a value.
Field.requiredIndicatorbooleanfalseMarks the label with a red asterisk while the control is required.
Field.namestringunsetField name used for submission and external form errors.
Field.disabledbooleanfalseDisables the field and its control.
Field.validationModeonSubmit | onBlur | onChangeonSubmit / FormWhen validation runs; a field override wins over Form.
Field.validate(value, formValues) => error | null | PromiseunsetCustom validation; asynchronous validation does not block onSubmit submission.
Field.validationDebounceTimenumber0Milliseconds between onChange validation callbacks.
Field.invalid / dirty / touchedbooleanuncontrolledExternal state overrides.
Field.actionsRefrefunsetExposes validate().
FieldError.matchboolean | validity keyfoundation matchingtrue always shows children; otherwise match the validation error.
FieldError.variantinline | tooltiptooltip in a floating Field, inline otherwiseinline 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.

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.