DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
FieldFieldsetFormFile UploadColor PickerMediaTrimmer
AI agentsContributing
  1. Components
  2. Field

Field

Single-line text input with label, helper and error rows, icons, clear button, in-field affixes, character counter, prefix/suffix slots and built-in validation wiring. Built on Base UI Field.

$ pnpm add @tessinaui/ui

When to use

  • Short, free-form, single-line text: a name, an email, a URL, a search term, a coupon code. If the answer is not a fixed set of options, a field is the right control.
  • A visible label on every field. Pass label; when the design hides it, use visuallyHiddenLabel so the name survives. Placeholder text is a hint, never the label.
  • Guidance before and failure after: supportingText explains the format up front, errorMessage (or validate) replaces it when the value is wrong. Keep both to one short sentence.
  • Format and unit cues that belong to the value: prefixText and suffixText for https:// or USD, and the prefix and suffix slots when the cue is itself a control such as a country code or a Search button.
  • Hard limits stated up front: set maxLength with showCharacterCounter so nobody discovers the limit by being truncated.
  • A mobile keyboard that matches the data: forward type, inputMode, enterKeyHint and autoComplete so people type less.

When not to use

  • For answers that span several lines. Use Textarea.
  • For a number people step or adjust. Use NumberField, which owns increment, decrement and number formatting.
  • For choosing from a known list. Use Select for a short list, or Combobox when people need to filter a long one.
  • For a search box with results or suggestions. Use Search, or SearchSuggest when suggestions appear as you type.
  • For a yes/no or one-of-few choice. Use Checkbox, Radio or Switch.
  • For a group of related fields. Wrap them in a Fieldset so the group has one name and one error; do not repeat a label across fields.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Field, FieldDropdown, FieldDropdownItem, FieldSkeleton } from "@tessinaui/ui";
example.tsx
<Field label="Email" placeholder="you@example.com" supportingText="We never share it" />

Usage rules

  • Always pass label. A field without a visible label needs visuallyHiddenLabel so the name survives for assistive tech — placeholder text is never a label.
  • supportingText is for guidance; errorMessage is for failure. When both are set the error replaces the supporting text (no layout shift next to an open keyboard).
  • Don't set intent="error" by hand to signal a validation failure — pass errorMessage (or validate) and let the field derive the error state, aria-invalid and the announcement.
  • Prefer prefixText/suffixText for static in-field text (https://, USD) and the prefix/suffix slots for real controls (FieldDropdown, Button).
  • Sizes below lg render 16px input text on phones so iOS Safari never zooms the page on focus; don't override the input font below 16px on mobile.

Examples

Default

Variants

Three shells: the default outline box, a filled well, and an underline bottom rule for dense forms. Filled is a tinted well with no outline at rest or on hover: the focus ring is its outline, and an error or other intent draws the border back. It matches the filled Textarea, and it reads best on a filled Form, where the panel sets the well apart. Prefer outline where a field sits straight on the page — the outline is what marks it as a place to type. Underline is square whatever rounded says, trades the focus ring for an inset 2px bottom band in the intent colour, and never gains a fill — disabled and read-only dim the rule instead. A floated inside label sits above the rule with no notch to cut. prefix / suffix slots keep their own boxes; pass variant to a FieldDropdown to match.

Intents

Five visual intents — none, error, warning, success, info — recoloring border, focus ring and supporting text. Intent is presentation; validation state comes from errorMessage/validate.

Validation

errorMessage for consumer-owned errors, validate (+ validationMode) for field-owned checks. Both render the same announced error row, join aria-describedby and set aria-invalid.

Sizes

Icons & Clearable

Decorative icons render as plain glyphs (no button semantics); pass …IconOnClick + …IconLabel to make one an actual named button. The clear button appears only while the field has a value, sits in the tab order as "Clear ‹label›", and Esc clears from the input.

In-field affixes

Static text inside the container: prefixText / suffixText, plus monospace for codes and IDs. Affix text is exposed to screen readers through the input's description.

Prefix & Suffix slots

<FieldDropdown> before the container and a <Button> after it, sharing the field's size and rounded. The character counter stays pinned to the input's trailing edge, never under the suffix button.

On-color

On a coloured plate, tone resolves from the surrounding Surface automatically (explicit prop wins, like Button): the well, text, icons, clear button and FieldDropdown derive from the surface ink contract; intent text uses the on-ink palette.

On an error, info or success plate a field has one text tier: supporting text, the counter, the "(optional)" marker, affixes, the placeholder and a resting inside label all render at full ink, because a dimmed copy of that ink cannot reach 4.5:1 against the well (see Surface › Muted inks on intent plates). Put the hint in the label or supporting text there, not in the placeholder. A placeholder still renders, at full ink, but it reads like a value.

States

optional, isLoading (spinner swaps a decorative trailing icon in place; an interactive trailing icon stays mounted and disabled, with the spinner on its input side; the clear button stays available; edits blocked, aria-busy), readOnly (value selectable, state paint suppressed), and FieldSkeleton.

Disabled

RTL

dir="rtl" aligns the label, helper and error rows right, moves the leading icon and prefixText to the right edge, and moves the clear button, trailing icon and suffixText to the left. An inside label floats from the right. Leave dir unset and the field follows the page's direction.

API Reference

Field

PropTypeDefaultDescription
labelReactNode—Visible label. Names the input via Base UI's id pairing
labelPlacement"top" | "inside" | "start""top"Logical label position: above, floating inside, or on the reading-start side (left in LTR, right in RTL)
labelWidth"sm" | "md" | "lg" | "xl" | "auto" | CSS length"md"Width of the start-label column so sibling fields share one control column — sm 6rem · md 8rem · lg 11rem · xl 14rem. Labels wrap, never truncate, and sit at the column's end, 12px from the field. Only applies with labelPlacement="start": the label stays beside the field while the field keeps at least 212px of its own row, and moves above it below that, whatever the screen width
requiredbooleanfalseNative required + red * (aria-hidden — the star stays out of the accessible name)
optionalbooleanfalseMuted "(optional)" marker after the label. Mutually exclusive with required
visuallyHiddenLabelbooleanfalseKeeps the label for assistive tech, removes it visually
infoTextstring—Small info text at the label row's end. Joins the input's description
supportingTextReactNode—Helper text below the field. Color and icon follow the intent; replaced by the error row while invalid
errorMessageReactNode—Error text below the field. Marks the field invalid, joins aria-describedby, announced politely. Implies intent="error" unless another non-none intent is set
validate(value, formValues) => string | string[] | null | Promise—Base UI Field validation callback — return message(s) to fail
validationMode"onSubmit" | "onBlur" | "onChange""onSubmit"When validate runs. Inside a Base UI Form, submit also focuses the first invalid field
validationDebounceTimenumber0Debounce for onChange validation, ms
intent"none" | "error" | "warning" | "success" | "info""none"Visual intent: border, focus ring, supporting-text color
variant"outline" | "filled" | "underline""outline"Shell: bordered box, tinted well or bottom rule. Filled draws no border until focus (the ring) or an intent, keeps its own fill when disabled, and cuts no notch for a floated inside label until an intent draws the border. Underline ignores rounded, focuses with an inset 2px bottom band instead of the ring, paints no fill in any state, and cuts no notch for a floated inside label
tone"default" | "on-color"surrounding Surface's toneon-color derives the well, text, icons and clear button from the surface ink contract for coloured/dark plates — same resolution as Button (explicit prop wins over the inherited Surface tone)
size"xs" | "sm" | "md" | "lg" | "xl""md"Heights 32/36/40/48/56px; ≥44px touch floor on phones. Control text follows the Button family's height-matched ladder (14px through lg, 16px at xl; 16px floor on phones) — input, prefix and a matched suffix Button always share one font. Meta text (label, supporting/error, info, counter) runs one step below: 12px through md, 14px at lg/xl. Icons, spinner and the clear button step the family square scale, identical whether decorative or interactive
rounded"none" | "sm" | "md" | "lg" | "xl" | "full""full"Radius cascade — container, clear button and FieldDropdown follow together
leadingIcon / trailingIconReactNode—Icon inside the container. Decorative by default (aria-hidden, not a button)
leadingIconLabel / trailingIconLabelstring—Accessible name — required when the icon is interactive
leadingIconOnClick / trailingIconOnClick() => void—Makes the icon a real IconButton
clearablebooleanfalseClear (×) button while the field has a value — tab-reachable, Esc also clears
onClear() => void—Called after clearing. Clearing also fires onChange with the empty value
prefix / suffixReactNode—Slots outside the container. FieldDropdown takes the Field's own size; a suffix Button is seated at the field's height automatically — any size you pass it is replaced, since the two families run different height ladders (fieldSuffixButtonProps(size) does the same for a Button placed beside a field outside the slot). rounded matches on all three
prefixText / suffixTextstring—Static text inside the container; exposed via the input's description
monospacebooleanfalseMonospaced value + affixes (codes, tokens, IDs)
isLoadingbooleanfalsePending state: aria-busy, edits blocked. The spinner swaps a decorative trailing icon in place; an interactive trailing icon stays mounted (disabled) with the spinner on its input side. Clear (button and Esc) stays available
readOnlybooleanfalseNative read-only; suppresses intent paint, error row and clear (state precedence: readOnly → disabled → error)
showCharacterCounterbooleanfalse"n / max" counter (needs maxLength). Limit is described to AT; remaining count announced politely near/at the limit
onValueChange(value: string) => void—Value-first convenience callback — fires alongside onChange, including with "" on clear
dir"ltr" | "rtl"—Direction. All layout is logical — start placement, icons, affixes and float origin mirror
stringsPartial<FieldStrings>English, Arabic under dir="rtl"Override the clear button's name — clear, or clearField ("Clear {label}") when the field has a string label — the optional marker, and the character counter's description and announcements (upToCharacters, charactersRemaining, limitReached). A count takes plural variants, key_<category> (charactersRemaining_few), picked by Intl.PluralRules; overriding a bare key drops the Arabic table's variants of it
wrapperClassName / containerClassName / classNamestring—Root / bordered container / <input> class hooks
labelPosition——Deprecated alias of labelPlacement (outside-top→top, outside-left→start); removal next minor

All other native <input> attributes (type, name, autoComplete, inputMode, enterKeyHint, …) are forwarded to the input. The root forwards wrapperClassName; the input carries className and the rest — the documented form-control exception shape.

FieldDropdown

PropTypeDefaultDescription
flagIconReactNode—Icon/flag element, clipped to a square box that follows rounded
textstring—Visible text (doubles as the accessible name)
labelstring—Accessible name — required for an interactive flag-only trigger
showChevronbooleantrueWith children, false makes it display-only (renders a <span>, no button for any modality)
childrenReactNode—<FieldDropdownItem> items — opens a Base UI dropdown menu
size / roundedsame as Field"md" / "full"Must match the parent Field
variant"outline" | "underline""outline"Pass the parent Field's. Underline draws only the bottom rule and focuses with the band; the flag box still follows rounded
onClickMouseEventHandler—Click handler when no children menu is used
menuZoomnumber—Visual scale for the body-portaled menu — pass the trigger's rendered scale inside transformed/scaled previews so popup and trigger keep one ratio
tone"default" | "on-color"surrounding Surface's toneOn-color well/text/chevron/focus ring from the surface ink contract. The menu PANEL deliberately stays the standard popover surface in both tones — a popup floats above the plate as its own surface (same panel as every other menu in the library)
disabledbooleanfalseMatches the Field's disabled surface; renders non-interactive

FieldSkeleton

PropTypeDefaultDescription
size / rounded / labelPlacementsame as Field"md" / "full" / "top"The placeholder is the Field's own box — touch floor included
variant"outline" | "filled" | "underline""outline"Squares the control bar for an underline Field; prefix and suffix bars stay boxed, like the real slots
showLabel / showHelperbooleantrue / falseLabel / helper bars
showPrefix / showSuffix / showCounterbooleanfalsePrefix-, suffix- and counter-shaped bars — the input-row boxes come from the Field's own cva, so heights and floors track every size
labelChars / helperChars / prefixChars / suffixCharsnumber14 / 24 / 12 / 9Real content lengths — bars are sized in characters
labelstring—Opt-in sr-only loading announcement. Silent by default — one announcement per loading region

Controlled Usage

example.tsx
const [value, setValue] = useState("");

<Field
  label="Search"
  value={value}
  onChange={(e) => setValue(e.target.value)}
  clearable
  onClear={() => setValue("")}
  showCharacterCounter
  maxLength={100}
/>

Clearing (button or Esc) fires a real onChange with the empty value, so form libraries stay in sync — in uncontrolled mode the DOM input is cleared too.

Migrating from labelPosition

OldNew
labelPosition="outside-top"labelPlacement="top"
labelPosition="inside"labelPlacement="inside"
labelPosition="outside-left"labelPlacement="start"

The old prop still works for one minor and warns in development. Values are logical: start follows the reading direction instead of lying in RTL.

Do / Don't

  • Do pair every field with a visible label; don't rely on placeholder text.
  • Do use errorMessage/validate for failures; don't hand-set intent="error" as the only error signal.
  • Do match size and rounded across Field and FieldDropdown, and leave size off a suffix Button — Field seats it at its own height. The button family's ladder is 32/40/48/56/96 against the field family's 32/36/40/48/56, so a Button placed beside a field outside the slot takes fieldSuffixButtonProps(size), never the Field's own size. Don't mix radii inside one composition.
  • Do use readOnly for values the user may copy; don't use disabled for that — disabled leaves the focus order and drops contrast.
  • Do keep maxLength + counter for hard limits; don't surprise users with silent truncation.

Keyboard input hints

Forwarded native attributes drive the right mobile keyboard: type="email", inputMode="decimal", enterKeyHint="search", and autoComplete (WCAG 1.3.5 — set it on personal-data fields: email, name, tel, postal-code, …).

Accessibility

Pattern: native labeled text input (no APG composite widget — the input, its label and its descriptions are the contract). Plumbing is Base UI Field: generated ids pair label↔input, and description, error, info text, affixes and the counter limit all join the input's aria-describedby.

KeyAction
Tab / Shift+TabMove through input, interactive icons, clear button, prefix/suffix controls
EscClear the value (when clearable and non-empty; the event stops there)
EnterSubmits the surrounding form (native); commits validation in Base UI Field
  • The error row lives in a pre-mounted polite live region, so inline errors are announced without moving focus; aria-invalid is set only while actually invalid — never on a pristine required field.
  • required is the native attribute (announced by AT); the * is aria-hidden so the name stays clean.
  • The clear button is named "Clear ‹label›" and is keyboard-reachable; decorative icons carry no role at all.
  • isLoading sets aria-busy on the input; the spinner itself is decorative (one announcement per state, owned by the control).
  • Character counter: the limit is a static description ("Up to N characters"); remaining count is announced politely at ~90% and at the limit, never per keystroke.
  • Touch: container and interactive icons keep a ≥44px floor on phones; input text is ≥16px on phones so iOS never auto-zooms.
  • Forced colors / high contrast ride the token system; the focus ring is the hard-gated --focus-ring pair.
  • Contrast (audit-gated, both themes): supporting text none 4.74/7.66 · error 4.77/5.20 · warning 7.14/13.68 (tinted foreground — raw amber fails) · success 4.95/11.13 · info 5.20/7.51 · floated label focus 17.93/15.72. The resting outline is soft on purpose (1.48:1 light, 1.91:1 dark): the label marks the field. Hover draws it in the text colour (19.80/18.97), focus hands over to the ring, and every intent draws its own border.
  • AT matrix: VoiceOver + Safari passed 2026-09-14 — an invalid field reads "Username, Spaces are not allowed, invalid data, edit text"; a helper reads "Email, We only use it for receipts, edit text". NVDA and JAWS are untested (§5.6).
Time SlotsFieldset

On this page

When to useWhen not to useInstallationUsageUsage rulesExamplesDefaultVariantsIntentsValidationSizesIcons & ClearableIn-field affixesPrefix & Suffix slotsOn-colorStatesDisabledRTLAPI ReferenceFieldFieldDropdownFieldSkeletonControlled UsageMigrating from labelPositionDo / Don'tKeyboard input hintsAccessibility