Skip to content

textbox, password, searchbox, and textarea

Four roles cover free-text entry. Without one, a text input is a stack of styled text elements standing in for a control — a label span, a conditional placeholder span, a value span — with no input anywhere. The component is render-only: no focus, no typing, no form participation, no label association, and the :placeholder-shown, :disabled, and :focus-visible selectors the css transformer emits target states the markup can never enter.

Status — React: Implemented • Web Components: Implemented • iOS: Not yet planned • Android: Not yet planned

They are four concepts rather than one with a modifier because the difference is not a web attribute. Each names a distinct native type on at least one platform, and a transform that knew only “text control” would have nothing to bind.

RoleWeb emissionWhere the value livesNotable
textbox<input type="text">value attribute—
password<input type="password">value attributeautoComplete must be set
searchbox<input type="search">value attributeUA clear affordance suppressed
textarea<textarea>element contentrows

Everything below applies to all four unless marked otherwise.

Roles

Apply the following roles to elements:

RoleTypeElement
textbox / password / searchbox / textareaControlElement of type: container
valuePartElement of type: text
placeholderPartElement of type: text
labelPartElement of type: text or nested instance prop of type: string
descriptionPartElement of type: text or nested instance prop of type: string
errormessagePartElement of type: text or type: instance
indicatorPartElement of type: text, type: glyph, or type: vector

The collapse

The role replaces its subtree with the control, driven by part roles rather than discovery:

DescendantResult
valueBecomes the control’s value
placeholderBecomes the placeholder attribute
labelLifted out as a <label htmlFor> sibling at the control’s depth
indicatorLifted out the same way — a required asterisk drawn inside the field’s stack — aria-hidden, keeping its render condition
Bare wrapperDropped silently
Text, glyph, image, or composed instanceDropped, named together in one warning
A slotDropped with a warning — place the role below the slot, not above it
A role this control does not acceptDropped, named in a warning

Land the role on the container holding only the control’s own layers. Leading icons, affordance buttons, and adjacent chrome belong beside that container. Where the layer tree offers no such container, restructure the layers rather than accepting the drops.

A password field’s reveal affordance is the clearest case: it is a control in its own right, usually an icon button instance, and carries button as a sibling of the collapsing container. Inside, it is dropped with a warning.

States

These roles are typically applied in conjunction with the following states:

StateEffectClassify?
disabledNatively disabled, enforced by the platformRecommended
readonlyNative readonlyRecommended
requiredNative requiredRecommended
invalidaria-invalidRecommended
placeholder-shownNative :placeholder-shownRecommended
hover / activeNative, on the control itselfRecommended, if the library styles it
focus / focus-visibleNative focus indicatorOptional — prefer the platform default

Read more about states in specs.

Specs

Component anatomy typically has elements and roles like:

anatomy:
labelAndValue:
type: container
role: textbox
label:
type: text
role: label
placeholder:
type: text
role: placeholder
value:
type: text
role: value

Figma

Annotate the following layers:

  • the field container as role:textbox — or password, searchbox, textarea
  • the value text layer as role:value
  • the placeholder text layer as role:placeholder
  • the label text layer as role:label
  • a required asterisk as role:indicator

The container must hold only those layers. Icons and affordance buttons go beside it, each with its own role.

React

Implementation

<TextInput label="Email address" value={email} onChange={(e) => setEmail(e.target.value)} />
<PasswordInput label="Password" autoComplete="current-password" />
<TextArea label="Notes" rows={4} />

Before / After

// before
<div className="text-input__label-and-value" data-element="labelAndValue">
<span className="text-input__label" data-element="label">{p.label}</span>
{p.displayedContent === "Placeholder" && (
<span className="text-input__placeholder" data-element="placeholder">{p.placeholder}</span>
)}
<span className="text-input__value" data-element="value">&nbsp;</span>
</div>
// after
<label className="text-input__label" data-element="label" htmlFor={controlId}>{p.label}</label>
<input
id={controlId}
type="text"
data-element="labelAndValue"
value={value}
onChange={(e) => { setValue(e.target.value); p.onChange?.(e); }}
placeholder={p.placeholder}
required={p.required}
/>

The placeholder conditional vanishes because it was compensating for the missing control — placeholder is an attribute, and the browser knows when to show it. The lift changes the label’s depth, so a float-label library needs restyling; one whose label is already a sibling pays almost nothing.

The same component as password differs by two lines — the emitted type, and the reveal affordance that survives by sitting beside the collapse:

<input id={controlId} type="password" data-element="labelAndValue" value={value}
onChange={(e) => { setValue(e.target.value); p.onChange?.(e); }}
autoComplete={p.autoComplete} />
<IconButton {...defaults} data-element="maskedAction" onClick={p.onMaskedActionClick} />

Contract

PropTypeTierGenerated body
onChange?(e: ChangeEvent<E>) => voidMUSTWired — sets the value, then calls the prop
onBlur?(e: FocusEvent) => voidSHOULDStub
onFocus? / onKeyDown?—COULDForwarded to the element
name?stringMUST— form submission identity
autoComplete?stringSHOULDPass-through, no default
rows?numberCOULDtextarea only
valuestring—The existing variant prop

E is the emitted element’s type — HTMLInputElement for three of the four, HTMLTextAreaElement for textarea.

Web Components

Same collapse inside the shadow root. Shadow DOM hides the inner input from a containing form, so the host participates directly through formAssociated and ElementInternals, reporting value and validity from willUpdate.

One thing form association does not recover: autofill is weaker across a shadow boundary, because password managers match fields heuristically on surrounding form structure they cannot see into. An explicit autoComplete matters more here than in light DOM.

iOS

Not yet planned. Intended binding:

RoleType
textboxTextField
passwordSecureField
searchboxTextField + .searchable
textareaTextEditor

placeholder becomes the field’s prompt. Tapping focuses and opens the keyboard; VoiceOver announces the label and the field type.

Android

Not yet planned. Intended binding:

RoleType
textboxTextField
passwordTextField + PasswordVisualTransformation
searchboxSearchBar
textareaTextField(singleLine = false)

One type plus three parameter values, where web has four spellings and iOS three types plus a modifier. Carrying the distinction in the concept name is what lets each transform map it with a lookup table; collapsing to one concept with modifier fields would move the same information into a second schema surface without removing any of it.

Additional details

Where textarea differs

<textarea> has no value attribute — its value is its content.

  • Never self-closing. An empty field emits no children rather than value=""
  • rows is emitted where the spec carries a height expressible as a line count; otherwise the stylesheet governs
  • type is never emitted — the element carries no type attribute

Where searchbox differs

<input type="search"> brings a user-agent clear affordance the design did not draw. The css transformer suppresses it alongside the other UA resets. Where the design draws its own clear affordance, it is a button sibling of the collapsing container, as the password reveal is.

Autofill is the consumer’s to set

A text field with no autocomplete is handled badly by browsers and password managers, so the prop is in the contract and the generated code says it should be set. The transform will not choose a value: the same password component is current-password on a sign-in form and new-password on a registration form, and guessing one harms the other.

Nor is it annotated. Annotations carry two categorical keys, role and action, each naming a concept from a governed vocabulary. Autofill is neither identity nor behavior; it is one of a long tail of platform attributes — input mode, spellcheck, enterkeyhint — and admitting the first makes the annotation surface an API with no principle saying where it stops.

The obligation is made visible instead:

/**
* Autofill hint. Set this — a password field without one autofills badly.
* `current-password` on a sign-in form, `new-password` on a registration form.
*/
autoComplete?: string;

Wired state

onChange follows the wired state model: internal state seeded from the value prop, set on input, consumer callback after. The field is typeable before a consumer attaches anything.

Prerequisite: a resolved value binding, either an existing variant prop or the value convention in conventions/specs.yaml. Without it the handler degrades to a stub and warns.

Accessible name

From the label part, lifted out beside the control and associated by htmlFor.

See also

  • value — the part driving the collapse, and where placeholder is described
  • checkbox — the proxy alternative for selection controls
  • button — reveal and clear affordances beside a text field
  • group — grouping several fields under one legend
  • Roles overview — the vocabulary and how roles are authored