DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

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.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Field, FieldDropdown, FieldDropdownItem, FieldSkeleton } from "@tessinaui/ui";
<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

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.

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 replaces the trailing icon in place, edits blocked, aria-busy), readOnly (value selectable, state paint suppressed), and FieldSkeleton.

Disabled

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. Only applies with labelPlacement="start", from sm up
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
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 takes fieldSuffixButtonProps(size) (the two families run different height ladders). 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: in-place spinner, aria-busy, edits blocked
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
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
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
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

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 seat a suffix Button with fieldSuffixButtonProps(size); don't hand the Button the Field's own size — the button family's height ladder is 32/40/48/56/96 against the field family's 32/36/40/48/56, so size="md" on both puts a 48px button beside a 40px field. 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 field outline is deliberately below the 3:1 boundary floor (§9 ledger 2026-08-24) — hover lifts it, focus hands over to the ring.
  • AT matrix: VoiceOver + NVDA passes pending — status stays beta until both are on record (§5.6).
FABFieldset

On this page

PlaygroundInstallationUsageExamplesDefaultIntentsValidationSizesIcons & ClearableIn-field affixesPrefix & Suffix slotsOn-colorStatesDisabledAPI ReferenceFieldFieldDropdownFieldSkeletonControlled UsageMigrating from labelPositionDo / Don'tKeyboard input hintsAccessibility