Field
Field is a compound layout primitive for building a single form field. It groups Field.Label, a control (Input, Select, Checkbox, etc.), helper / hint text (Field.Description), and validation errors (Field.Errors, Field.ErrorList, Field.ErrorItem). Stack multiple fields together with Field.Group, or (for radios and related checkboxes specifically) wrap them in a Field.Set + Field.Legend for a properly-named semantic group.
Field.Label wraps the mantle Label for ergonomic field composition. It keeps the same click-to-focus, disabled, and typography behavior as Label; see the Label docs for the full surface.
Label-to-control association is automatic inside a Field.Item. Field.Item generates a stable control id. Field.Control splats that id onto its focusable child, and Field.Label points htmlFor at it. The required name on Field.Item is the single source of truth for the control's form name. That lets you write <Field.Label>Email</Field.Label> next to <Field.Control><Input /></Field.Control> with no matching htmlFor / id pair and no separate useId() call. To set the id yourself, pass id on Field.Item, for example when a Playwright locator or an anchor link must find the control by id. Field.Control and Field.Label both follow it, and the wrapping <div> carries no id. Field.Label has no htmlFor prop and Field.Control has no id prop, so Field.Item is the one owner. Render the focusable element inside Field.Control so the id lands on it.
Wrap the focusable control in Field.Control so Field.Item can associate descriptions and rendered errors via aria-describedby / aria-errormessage. A rendered Field.Errors or Field.ErrorList infers error state once the page hydrates. When you need to override that state, or when the error must be in the server HTML, pass validation on Field.Item (see Server-rendered errors).
Field.Item owns the full control contract. When a focusable control is wrapped in Field.Control, the surrounding Field.Item owns its id, name, aria-describedby, aria-errormessage, aria-invalid, and validation; anything supplied on the child element (or on Field.Control) is overwritten. There's one place to override: Field.Item itself, with id, name, or validation. To opt out of the contract entirely, render the control without Field.Control.
Pick something memorable — you can change it later.
Common mistakes
The canonical slot order inside a Field.Item is Field.Label → Field.Control → Field.Errors → Field.Description. Field.Item renders its children in source order and the JSX stays valid no matter how you arrange them, so a misplaced slot fails silently rather than erroring. The most common mistake is putting Field.Description (help text) above the control instead of below it.
- Enter a valid recovery code.
This renders below the input.
Error messages
Use Field.Errors below the control when validation fails. It accepts string messages, trims them, filters empty values, removes duplicates, and renders a semantic <ul> with <li> items so screen readers announce the errors as a list. When the list renders, Field.Item infers error state after hydration so the visual state and ARIA invalid state stay in sync. For errors that render on the server, see Server-rendered errors below.
For custom list markup or rich error content, compose the lower-level Field.ErrorList and Field.ErrorItem parts directly. A single error is still a list of one.
When a Field.Description follows directly after Field.Errors or Field.ErrorList, Field.Description automatically collapses the parent's gap-1.5 with a matching negative top margin so the errors + helper read as one tight block, matching the figma. Pass any margin utility on Field.Description (e.g. className="mt-1", className="mt-0") to override.
- Enter a valid email address.
We'll never share your email.
Multiple errors
Pass multiple messages to Field.Errors when a validator produces more than one message at once (e.g. minLength + pattern + custom rule). Items stack tightly so the whole error block reads as one unit.
- Must be at least 12 characters.
- Must include a number.
- Must include a symbol.
Use a password manager to generate one.
When errors arrive from a validator as an array (e.g. field.state.meta.errors from TanStack Form), map them to messages and pass them to Field.Errors. Empty, blank, missing, or duplicate messages render nothing and do not add an error message reference. Reach for toErrorMessages to handle the mixed string / { message } / nullish shapes that TanStack Form yields; see With TanStack Form below.
Manual error list
Prefer Field.Errors when you already have plain string messages. Compose Field.ErrorList and Field.ErrorItem directly when you need custom list contents, richer message markup, or a polymorphic list element. Use one or the other in a Field.Item: Field.Item owns a single errors slot ID, and Field.Errors is itself a Field.ErrorList under the hood, so rendering both would duplicate the id on the page.
- Must be at least 3 characters.
- Use letters, numbers, hyphens, or underscores.
Pick something memorable.
Server-rendered errors
Field.Item infers the "error" state on the client: a mounted Field.Errors or Field.ErrorList registers with Field.Item after hydration. On the server, nothing runs that registration. The server HTML carries the error list. Until the page hydrates, the control is not flagged aria-invalid="true" and has no aria-errormessage, and the item has no data-validation="error". The control keeps its neutral border. A screen reader doesn't announce it as invalid.
When errors come back from the server, for example from a React Router action, set validation on Field.Item from the same messages. An explicit validation is in the server HTML from the first paint:
validation accepts false, so the short-circuit yields false with no messages and "error" with at least one. Client-only validators, such as the TanStack Form example below, don't need this: they run after hydration, where the inference already works.
Optional fields
Use Field.Optional inside a <Field.Label> to mark a field as optional. It renders a muted "(Optional)" suffix at text-sm regular weight so it reads as secondary metadata without competing with the bolder label text. Defaults to the literal "(Optional)"; pass children to translate or replace.
Visible on your public profile.
Place Field.Optional inside the <Field.Label> so screen readers announce it as part of the accessible name (e.g. "Nickname, Optional, edit text"). The label's flex items-baseline gap-1 keeps the suffix on the same baseline as the bolder label text.
Help icon next to the label
When the label needs a sibling affordance that can't live inside the <Field.Label> itself (most commonly an interactive help-icon button), wrap the label and its decorations in Field.LabelRow. It's a horizontal flex container with items-center and gap-1 so the label, optional suffix, and help icon all sit on a shared center line. (Center alignment, not baseline: SVG icon buttons have no text baseline, so items-baseline would push them above the label text.)
Putting an interactive button inside <label> is a footgun: clicks on a <label> forward focus to the associated control, so a help button nested in a <Field.Label> would steal that behavior. Field.LabelRow is the way out.
Reach for the bundled Field.Help / Field.HelpTrigger / Field.HelpContent rather than wiring Popover + IconButton + an icon by hand: it's the same composition, three thin wrappers, with sensible visual defaults (ghost IconButton, xs size, default Phosphor QuestionIcon, text-body color). Field.HelpTrigger still requires a contextual accessible label so repeated help icons do not all announce the same thing. Popover under the hood (not Tooltip), so the affordance is reachable on touch devices.
You can find this in the ngrok dashboard.
Field.HelpTrigger accepts every IconButton prop, so swap the icon or change the appearance without dropping down to raw Popover:
Multiple fields
Wrap multiple Field.Items in a Field.Group to stack them with consistent gap-4 spacing. This is the default for any form with more than one field: pure layout, no <fieldset> semantics.
At least 12 characters.
Radio and checkbox groups
Field.Set renders a real <fieldset>. Paired with Field.Legend, it gives a related set of controls a single accessible name (so a screen reader announces "Notification frequency, group, Daily"). Reach for it specifically when the grouping is itself the question the user is answering: most commonly a RadioGroup (one question, many possible answers), or a set of related checkboxes (preference toggles, permission flags, opt-ins).
When not to use it. Stacking unrelated fields under a shared header is not a semantic group; Field.Group (above) is the right call. A <fieldset> around plain inputs reads as noise to assistive tech and looks visually heavy. If you find yourself reaching for Field.Set to add a section title to a form, render a regular heading instead.
Keep the Field.Legend self-contained: it is the accessible name for the group. Field.Set does not auto-associate Field.Description; if a group needs extra instructions announced by assistive technology, wire your own description with aria-describedby.
With TanStack Form
Field is designed to slot directly into TanStack Form. Pass field.name as the name prop on Field.Item: it splats onto the focusable control inside Field.Control and generates a stable id that wires Field.Label ↔ control association automatically. Wrap the input in Field.Control so descriptions and rendered errors are associated with the control.
field.state.meta.errors is a mixed array: TanStack Form yields plain strings, { message } issue objects (Zod / Standard Schema), thrown Error instances, and falsy slots all in one place. Pass it through toErrorMessages to get a clean string[] for Field.Errors. The list renders nothing when the result is empty, and infers error state only when it has messages, so it's safe to leave mounted unconditionally:
Without toErrorMessages
toErrorMessages is a convenience: Field.Errors itself only needs a string[]. If you already know your validator's error shape (or you want to filter / re-format messages first), map the array inline and skip the helper. The example below mirrors the TanStack Form example above, but pulls .message off each entry by hand:
Field.Errors already trims each string, filters empty / nullish / false entries, and renders nothing when no messages remain, so passing undefined / empty strings through the .map is safe and behaves the same as filtering them out. Use the inline form when the helper's mixed-shape handling isn't pulling its weight (e.g. a validator that always emits { message: string }, where a bare error?.message map is enough), and reach for toErrorMessages when you want a single call that handles the full TanStack Form error array shape.
Composition
Most forms only need a Field.Group of Field.Items; that's the default tree. Each Field.Item may render at most one Field.Description and one Field.Errors or one Field.ErrorList (not both); they share a single slot ID owned by the item.
For radios or related checkboxes (where the grouping is itself the question being answered), wrap the controls in a semantic Field.Set + Field.Legend instead:
Skip Field.Set for unrelated fields stacked together: <fieldset> semantics around plain inputs read as noise to assistive tech.
Polymorphism
Layout parts plus Field.Description, Field.Optional, and Field.ErrorList accept an asChild prop. When true, the part renders its single child instead of its default element, forwarding all class names, data-* attributes, and the ref onto that child. Field.Set, Field.Legend, and Field.ErrorItem always render <fieldset>, <legend>, and <li> respectively, because those elements are the semantics those parts exist to carry.
- Fix this first.
- Then fix this.
API Reference
Field.Label
Field.Label wraps Label and always points htmlFor at the surrounding Field.Item's control id. htmlFor is not a prop; set id on Field.Item to choose the id. Use it inside Field.Item to give the control an accessible name and click-to-focus behavior.
See the Label docs for association patterns, disabled state, and typography behavior.
All props from Label except htmlFor.
Field.LabelText
Static, label-styled text for a Field.Item row that does not caption a focusable control. Renders a <p> (not a <label>) with the same typography as Field.Label, so a read-only row composes cleanly alongside real form fields without misrepresenting itself as a control caption.
When to use
- A
Field.Itemrow that displays a derived or system-managed value — owner, created-at, computed status, an identity card — where there is no<input>,<select>,<button>, or other focusable target to caption. - Read-only summary rows inside a sheet or details panel that should visually align with sibling
Field.Items but have no editable control. - Anywhere you'd otherwise hand-roll a
<p>styled to mimicField.Labeltypography. UseField.LabelTextso the intent is explicit at the call site.
When not to use
- The row has a focusable control. Use
Field.Labelso thehtmlFor↔ control association, click-to-focus, and accessible name are wired automatically. - For body copy, section headings, or any text whose typography only happens to match by accident. Reach for a heading or a plain
<p>instead —Field.LabelTextexists specifically to slot into theField.Labelposition of aField.Item.
Owner
Ada Lovelace
The user or service user that owns this API key.
All props from p, plus:
Field.Item
A single form field. Renders a plain <div> with flex flex-col gap-1.5 so Field.Label, control, and helper / error siblings stack tightly together. No implicit ARIA role: the <label htmlFor> ↔ control association already carries the right semantics for a single field. It owns the description/error IDs and validation state that Field.Control applies; rendered errors infer "error" after hydration unless validation is set. For a server-rendered error, set validation (see Server-rendered errors).
Single-slot constraint. A Field.Item owns one description ID and one errors ID, so render at most one Field.Description and one Field.Errors or Field.ErrorList (not both) per item. A second instance would duplicate the slot id in the DOM — pass multiple messages to one Field.Errors, or multiple Field.ErrorItem children to one Field.ErrorList, instead.
When to use: for every individual field — text inputs, selects, single checkboxes, switches, password inputs, etc.
All props from div except id, plus:
Field.Control
Applies Field.Item description, error, identity, and validation state to a single control. It always behaves like an asChild slot: pass one child element to receive the generated props, or use a function child to place those props manually. Mantle compound controls that consume FieldControlContext can be wrapped at the root when their docs show that composition; otherwise use the render-prop form to place the generated props on the actual focusable element. Override validation on the surrounding Field.Item. id is not a prop here either; set it on Field.Item.
For the simple element-child form, see the primary example above; to wrap a compound control (Select, MultiSelect) at its root, follow the composition shown in that control's own docs.
Use a function child when the focusable element is nested inside a wrapper that Slot cannot reach — for example a <label>-wrapped native checkbox where the <input> is the focusable target but the outer <label> is the rendered element. The function receives the generated ARIA props and the caller spreads them onto the inner control.
- You must accept the terms to continue.
Field.Group
Layout container that stacks multiple Field.Items vertically with gap-4. Renders a plain <div> — pure layout, no semantics.
When to use: any time a form has more than one field. This is the default way to compose multiple Field.Items. Reach for Field.Set + Field.Legend instead only when the grouping itself carries semantic weight (radios, related checkboxes).
All props from div, plus:
Field.Set
Renders a semantic <fieldset> with default browser styling reset (no border, no padding, min-width: 0). Pair with Field.Legend to give the surrounding controls a single accessible name.
When to use: specifically for radios (one question, many possible answers) or sets of related checkboxes (preference toggles, permission flags). Skip it for stacking unrelated fields — Field.Group is the right call there. A <fieldset> around plain inputs reads as noise to assistive tech.
Field.Set does not automatically associate Field.Description. If the group needs additional announced instructions beyond the legend, pass aria-describedby to Field.Set and point it at your own text element.
All props from fieldset.
Field.Legend
Caption for a Field.Set. Renders a <legend> styled to match the mantle Label typography. Always render a Field.Legend inside a Field.Set — it's what gives the surrounding fieldset its accessible name.
Carries a default mb-1.5 so the legend sits 6px above the next sibling. The margin is on the legend (not the parent Field.Set's flex gap) because <legend> has special browser rendering inside a <fieldset> that ignores the parent's flex gap. Override with any mb-* utility passed on Field.Legend.
All props from legend.
Field.LabelRow
Horizontal layout container for the label area of a field — a flex row with items-center and gap-1. Use when the label needs a sibling affordance that can't live inside the <Field.Label> itself, like a help-icon Field.HelpTrigger. Center-aligned (not baseline) so SVG icon buttons sit visually centered next to the label text.
All props from div, plus:
Field.Help
Popover.Root wrapper for the help-affordance pattern. Composes with Field.HelpTrigger and Field.HelpContent. Forwards all Popover.Root props (open, defaultOpen, onOpenChange, modal, etc.).
Field.HelpTrigger
Popover.Trigger wired to a ghost-appearance IconButton with a default Phosphor QuestionIcon. Requires a contextual accessible label. Pre-styled with text-body so the icon reads as subtle metadata at rest; IconButton's ghost hover:text-strong brightens it on interaction. Carries a default -my-0.5 so the 24px xs button keeps a full click target while contributing only 20px to the Field.LabelRow flex line — matching the label's text line-height so the label is not pushed off-center.
Accepts every IconButton prop, with sensible visual defaults. label remains required:
Field.HelpContent
Body of a Field.Help popover. Forwards every Popover.Content prop — see the Popover docs for the full surface (side, align, preferredWidth, etc.).
All props from div, plus:
Field.Optional
Inline "(Optional)" suffix to mark a field as optional. Renders a <span> in text-muted at text-sm / font-normal. Default content is the literal string "(Optional)" — pass children to translate or replace.
All props from span, plus:
Field.Description
Helper / hint text. Renders a <p> in the muted body color. Use inside Field.Item, below the control, to clarify expected format or constraints for that single field. Render at most one per Field.Item — Field.Item owns a single description slot ID and applies it via context.
When rendered inside Field.Item, Field.Description receives an automatic generated ID that Field.Control merges into the control's aria-describedby. The ID is owned by Field.Item and is not consumer-overridable. When Field.Description is rendered immediately after Field.Errors or Field.ErrorList, it automatically collapses the parent's gap-1.5 via a -mt-1.5 so errors + helper read as a single tight block. The rule's specificity is flattened to (0,1,0) so any margin utility you pass on Field.Description (e.g. mt-1, mt-0) overrides cleanly.
All props from p except id, plus:
Field.Errors
Convenience renderer for validation messages. Accepts an array of strings, trims each message, filters empty, nullish, and false values, then renders a Field.ErrorList with one Field.ErrorItem per remaining message. When rendered inside Field.Item, a non-empty list receives an automatic generated ID and infers "error" validation after hydration unless Field.Item overrides it. Field.Control merges that ID into the control's aria-describedby and aria-errormessage when the field is invalid. The ID is owned by Field.Item and is not consumer-overridable. For a server-rendered error, set validation on Field.Item (see Server-rendered errors).
Render at most one Field.Errors per Field.Item, and do not combine it with Field.ErrorList in the same item — both render a single shared errors slot. Pass multiple messages to one Field.Errors for the multi-error case.
Field.Errors owns its generated list items and does not accept children or asChild. Use Field.ErrorList / Field.ErrorItem directly when you need custom list contents or polymorphic list markup.
All props from ul except children and id, plus:
Field.ErrorItem
A single error message for a field. Renders an <li> in text-danger-600, so it must be nested inside a Field.ErrorList. Empty or blank children render nothing. A single error is a list of one.
All props from li.
Field.ErrorList
Wraps one or more Field.ErrorItem children in a semantic <ul> with role="list" so a list of validation errors is announced as a list by screen readers, including Safari/VoiceOver combinations that drop list semantics when list styling is removed. Renders nothing when given no renderable children. When rendered inside Field.Item, a non-empty list receives an automatic generated ID and infers "error" validation after hydration unless Field.Item overrides it. Field.Control merges that ID into the control's aria-describedby and aria-errormessage when the field is invalid. The ID is owned by Field.Item and is not consumer-overridable. For a server-rendered error, set validation on Field.Item (see Server-rendered errors).
Render at most one Field.ErrorList per Field.Item, and do not combine it with Field.Errors in the same item — both render a single shared errors slot. Put multiple Field.ErrorItem children inside one list for the multi-error case.
Strips the default browser <ul> styling (no bullets, no padding, no margin) and stacks items as a flex column with no gap so consecutive errors read as a single tight block. Pass asChild with an <ol> if the list is meaningfully ordered.
All props from ul except id, plus:
toErrorMessages
Normalize a TanStack React Form field's meta.errors array (or any mixed array of string / { message } / nullish / false entries) into a clean string[] ready to pass to Field.Errors. Trims whitespace, drops empty / whitespace-only entries, and removes duplicates so callers don't have to filter again.
Use this for TanStack Form with Zod, Standard Schema, or thrown Error validators — field.state.meta.errors is a union of all of those shapes, and this helper folds them down to plain strings without coupling Mantle to a specific form library. For other validators where the error shape is already a string[], skip the helper and pass the array directly.
- Enter a valid email.
Returns string[]. FieldError is { message?: string } \| string \| null \| undefined \| false.