Field

Developer Experience (DX)

How developers use the Field, with one typed API that labels, describes and validates every form control, without changing any of them.

Next, let's look at how developers use the Field. It has one API for every control, with the names the controls already use, it's typed end to end, and it does its work through attributes, so none of the controls needs to know it's there.

import { useState } from "react";
import { Field } from "@/components/Field";
import { Input } from "@/components/Input";

export function Newsletter() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  return (
    <Field id="email" label="Email address" hint="We'll only send you the newsletter" error={error} required>
      <Input
        type="email"
        autoComplete="email"
        value={email}
        onChange={(event) => setEmail(event.target.value)}
        onBlur={() => setError(validateEmail(email))}
      />
    </Field>
  );
}

validateEmail stands in for your own check, which returns an error message, or an empty string when the address looks right.

Consistent

The Field's props are the ones the controls already have, or would have if they rendered their own label. These names mean the same thing everywhere in Open Components:

NameKindWhat it means
idPropThe control's id, which its label, hint and error point at
labelPropThe text that names the control
hintPropHelp that describes the control, the same as the Radio Group's
errorPropWhat's wrong and how to fix it, which makes the control invalid while it's set, the same as the Radio Group's
requiredPropThe control's native required
disabledPropThe control can't be used
sizePropxs, sm, md, lg or xl, with the same sizes everywhere

The Input, the Textarea, the Checkbox and the Switch don't have a label, hint or error prop, so that each of them stays one control that fits any layout, and the Field puts them together. The Field also leaves a few props out, on purpose:

  • There's no optional prop, or a marker for required. Which fields to mark is a decision about the whole form, so it's words you put in the label, like "Company (optional)".
  • There's no invalid prop. An invalid control needs a message that says what to fix, so error is what makes it invalid.
  • There's no labelPosition. Where the label goes follows from the control, as under Controls, so every switch and every text control in a product looks the same.
  • There's no name or value, and nothing to bind. They belong to the control, which the form reads.

Type-safe

Props and slots are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like size="medium".

PropTypeDefaultDescription
idstringThe control's id. Without it, the control's own id, then its name
labelstringNames a text control or a Switch, as its <label>
hintstringHelp people need before they fill the control in
errorstringWhat's wrong and how to fix it. While it's set, the control is invalid
requiredbooleanfalseSets the control's native required, except on a Checkbox Group
disabledbooleanfalseDisables the control
size"xs" | "sm" | "md" | "lg" | "xl"The text size, which also goes to a component control. Without it, the control's own
SlotDescription
defaultOne control: an Input, a Textarea, a Checkbox, a Switch, a Checkbox Group, a Radio Group or a native <input>, <textarea> or <select>

The Field fires no events of its own, since it holds no state. Listen to the control, as you would without a Field. We also export FieldProps, which comes in handy for data-driven forms:

import type { FieldProps } from "@/components/Field";

const fields: (FieldProps & { name: string })[] = [
  { name: "email", label: "Email address", required: true },
  { name: "company", label: "Company (optional)" },
];

Composable

The Field doesn't render the control. You do, and the Field adds what links it to its label, hint and error: an id, aria-describedby, aria-invalid, required and disabled, which every control already puts on its native element, and size, which every control already takes as a prop. None of them needs a prop, a context or any code that knows about the Field, and their own pages still hold inside one.

How the Field hands them over follows each framework's idiom. In React and Vue, it adds them to the one element you put inside it. In Angular, the Field is a directive on its own <div>, which finds the native control inside it and sets them there, so give the native control its id, since a static attribute stays on the Field's own element. Vanilla writes them out.

In Svelte, Solid and Astro, whose children have already rendered by the time the Field could change them, it hands them to a snippet or a function, which spreads them onto the control. The Field can't read that control's props, so give the Field its id, and spread what it hands you after your own attributes only where you want the Field's to win.

What you set yourself is kept, with a few rules for when you and the Field both set the same thing:

  • The Field's id wins over the control's, and the control's id, then its name, are used when the Field has none.
  • aria-describedby is joined: the Field's hint and error first, then whatever you set on the control.
  • aria-invalid comes from the Field while it has an error, and is yours otherwise. A Textarea over its limit stays invalid either way.
  • size from the Field wins over the control's, and the Field takes the control's own when it has none. A native <input>, <textarea> or <select> doesn't get it, since its own size is a number of characters or rows.
  • Around a Radio Group, the Field's hint and error win over the group's own. It's best to set each one in one place.

Forms

The Field adds no element that a form reads, so the control's name and value reach the form the way they always do, and your form library binds the control, not the Field. That also means the label, the hint and the error are in the server's markup, and the form works before your JavaScript loads.

The error is yours to set, from whatever decides it: your own checks as people leave a control, the browser's validity on submit, or a response from your server. You'll find the whole flow, with focus and a status message on a failed submit, under Check values when people leave or submit.

With other components

The Field works with every form control in Open Components, and sets the same things on each one, in its own way:

ControlWhat the Field sets
Inputid, aria-describedby, aria-invalid, required and disabled on its <input>, and its size prop
TextareaThe same, on its <textarea>, which adds its count to the description
CheckboxThe same, on its <input>. No label, since it has its own
SwitchThe same, on its <input>
Checkbox Groupid, aria-describedby, aria-invalid and disabled on its <fieldset>, and its size prop. Never required
Radio GroupIts hint, error, required, disabled and size props

The Select, on our Roadmap, will work with the Field the same way.

Controllable

The Field has no state of its own. Its label, hint and error are props, so it shows exactly what you pass it, and the control keeps its value, bound or not, as on its own page. When the error goes away, the control's invalid state and description go with it, in the same render, so the three can't disagree.

DX Checklist

RuleLevelScopeRequirementBasisCheck
field/consistent-namesShouldComponentIts props use the names the controls use, like hint, error, required, disabled and size, with the same meaningsOpen ComponentsReview
field/typedShouldComponentIts props and slot are typed, and FieldProps is exportedOpen ComponentsType check
field/controls-unchangedMustComponentIt works with every form control through the attributes they already take, without a prop, a context or code of theirs that knows about itOpen ComponentsUnit test
field/own-attributesShouldComponentAn aria-describedby set on the control is kept after the Field's, and an aria-invalid set on it is kept while the Field has no errorOpen ComponentsUnit test
field/one-controlShouldUsageA Field holds exactly one controlOpen ComponentsUnit test
field/statelessShouldComponentIt holds no state of its own, so it shows exactly the label, hint and error it's givenOpen ComponentsReview

FAQ

Copyright © 2026