Skip to content
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
Getting startedWebReactSwiftUIJetpack ComposeFlutterReact NativePresetsYour own design systemUsing Tanrei with AI agentsBrandDesign tokensDesign systems and customizationTheme StudioColorTypographyIconsShapeElevationMotionInteraction statesBehaviourLayoutAccessibilityWordsColor tokensTypescale tokensShape tokensElevation tokensMotion tokensState tokensSpace and measure tokensComponentsButtonMain action buttonSegmented buttonButton groupButton with menuToolbarCopy buttonCheckboxRadio buttonSwitchChipMenuSliderSlider fieldCalendarDateTimePickerField groupRadio groupCheckbox groupSelectSelect fieldMenubarFile uploadFile picker AdvancedDrop zoneText input AdvancedText fieldChip fieldFieldCombobox AdvancedCombobox fieldNumber input AdvancedNumber fieldColor input AdvancedColor fieldOne-time-code input AdvancedOne-time-code fieldEmbedded text input AdvancedChat composerCardDividerModal layer AdvancedDialogListList rowBottom sheetSide sheetCarouselCode blockTablePopoverStepsAccordionDescription listFigureApp shellPage headerTree viewTimelineImage previewHandleResizable frameScroll areaBadgeProgress indicatorMeterNumber tickerLoading indicatorSnackbarTooltipKeyboard shortcutTagInline codeCalloutAvatarEmpty stateTag groupSkeletonFile itemSwatchMarquee textTabsTab viewApp barNavigation barNavigation drawerSearchLinkPaginationPrevious and next linksStepperBreadcrumbsTable of contentsLink cardFAB menuStatusHow Tanrei is builtContributingLicense and attribution
HomeComponentsText field

Text field

Lets people enter and edit text — one line, or several that follow the text or are resized by hand — or a date or a time entered unit by unit in the locale's form, or code held exactly as it is written, with a label that can say the field is required or optional, optional icons, prefix and suffix, supporting text, a character count and a picker button.

content_copyView as Markdown
Category
Text inputs
Tokens
comp.field
Level
Composed
Built from
Field, Text input

Overview

Web
tanrei-text-field
schedule
SwiftUI
TanreiTextField
schedule
Jetpack Compose
TanreiTextField
schedule
Flutter
TanreiTextField
schedule
React Native
TextField
schedule
Variants not shipped in
tanrei: filled, outlined

Generated from docs/status/status.json, specs/text-field.spec.json

A revised spec, not yet on every platform

This page describes the text field as the spec has it now: a stacked variant, the default, with its label above the field; filled and outlined kept for the Material and cupertino presets only; heights of 32, 40 and 56 (40, 48 and 56 for a filled or outlined field, whose label floats inside it); date and time types, with a picker button; a multi-line field that follows its text or is resized with a handle (resize); a date or a time entered unit by unit, in segments; and required and optional, which mark a field as required or optional. The web component already does all of this, and so do the demos. A field can also hold code (type="code", and wrap for its lines): the web has built it, and no other platform has yet. The text field is a field around a text input, as this page describes it: the web has built that too, and there the required attribute is gone. SwiftUI, Compose, Flutter and React Native are marked lagging above: until they catch up they ship the previous revision, outlined by default at 36 and 40, with a required flag that draws no mark, and with no stacked variant, no date or time types, no picker button and no resize modes, and their samples show that revision.

Stacked, the default: a required field, and a date with its picker button

Use a text field wherever a form asks for text: it is the ordinary case, ready to use, with a label, the box, a line of help, the words for a wrong value and a count.

Built from a field and a text input. The field is the frame: the label, the required or optional mark, the help, the error and the count. The text input is the control: the box and everything typed in it. The text field draws nothing of its own and has no token: every property it has is a property of one of the two, given to it unchanged.

It is a block as wide as the place it is put in, and its box fills it. It has no width of its own: for a narrower field, give it a narrower place.

Playground

textemailpasswordnumbersearchtelurlcodedatetimedate-time
Each control sets one property of the live component, and the code follows. The page’s address keeps what you set, so a link opens it as you left it. The code is for the web. For the other platforms, see “Platform name mapping”.

Usage

<form>
  <tanrei-text-field name="email" label="Email" type="email" autocomplete="email" required required-text="Required"
    supporting-text="We only use it to sign you in."></tanrei-text-field>
  <tanrei-text-field name="company" label="Company" optional optional-text="Optional"></tanrei-text-field>
  <!-- Follows its text from 3 lines to 8, then scrolls. -->
  <tanrei-text-field name="message" label="Message" multiline min-rows="3" max-rows="8"></tanrei-text-field>
  <!-- Resized by hand, with a handle. -->
  <tanrei-text-field name="notes" label="Notes" multiline resize="manual" resize-label="Resize notes"></tanrei-text-field>
</form>

<!-- The box alone, in a toolbar: a text input, named by its own label. Importing the text field defines it. -->
<tanrei-text-input label="Search" type="search" placeholder="Search components">
  <span slot="leading-icon">search</span>
</tanrei-text-input>

<!-- Code: left to right in the code face in every page direction, never corrected or spell-checked. -->
<tanrei-text-field label="Pattern" type="code" value="tanrei.comp.button.*.container.color"></tanrei-text-field>
<tanrei-text-field label="Webhook body" type="code" multiline min-rows="4" supporting-text="JSON"></tanrei-text-field>
<!-- No wrapping: each line of the code stays on one line, and a long line scrolls inside the field. -->
<tanrei-text-field label="Rows" type="code" multiline no-wrap></tanrei-text-field>

<!-- A date entered unit by unit in the locale's order, held as 2026-09-27, with a picker button. -->
<tanrei-text-field id="start" label="Start date" type="date" min="2026-01-01" disabled-dates="2026-05-01 2026-12-25"
  invalid-text="Finish the date." unavailable-text="Choose a working day in 2026 or later."
  picker picker-label="Choose start date" segment-empty-text="Empty"></tanrei-text-field>
<tanrei-date-time-picker id="start-picker" presentation="docked" anchor="start" label="Start date"></tanrei-date-time-picker>

<script type="module">
  import '@tanrei/web/components/text-field';
  const email = document.querySelector('[name="email"]');
  email.addEventListener('input', () => draft(email.value)); // every edit
  email.addEventListener('change', () => save(email.value)); // focus left or Enter, with a changed value
  // Validation is yours: the field never sets error itself.
  email.error = submitted && !isValid(email.value);
  email.errorText = 'Enter a complete email address.';

  // The field asks; the product presents the picker and writes the value back.
  const start = document.querySelector('#start');
  const picker = document.querySelector('#start-picker');
  start.addEventListener('pickerrequest', (e) => {
    Object.assign(picker, { value: e.detail.value, min: e.detail.min, max: e.detail.max, disabledDates: start.disabledDates, open: true });
  });
  picker.addEventListener('change', () => (start.value = picker.value));
  picker.addEventListener('close', () => (picker.open = false));
</script>
import { TextField } from '@tanrei/react';

<TextField
  label="Email"
  type="email"
  autocomplete="email"
  value={email}
  onInput={(e) => setEmail(e.currentTarget.value)}
  onChange={() => save(email)}
  required
  requiredText="Required"
  error={submitted && !isValid(email)}
  errorText="Enter a complete email address."
/>

<TextField label="Message" multiline minRows={3} maxRows={8} value={message}
  onInput={(e) => setMessage(e.currentTarget.value)} />

// A date: the value is ISO 8601, or empty while a segment is empty.
<TextField
  label="Start date"
  type="date"
  min="2026-01-01"
  value={start}
  invalidText="Finish the date."
  unavailableText="Choose a date in 2026 or later."
  segmentEmptyText="Empty"
  picker
  pickerLabel="Choose start date"
  onChange={(e) => setStart(e.currentTarget.value)}
  onEntryError={(e) => report(e.detail.reason)}
  onPickerRequest={() => setPickerOpen(true)}
/>
import Tanrei

TanreiTextField("Email", text: $email, type: .email,
                supportingText: "We only use it to sign you in.",
                errorText: "Enter a complete email address.",
                error: submitted && !isValid(email),
                textContentType: "email", // an HTML autofill name
                onCommit: save,
                // Pass the action here: an enclosing .onSubmit runs before onCommit.
                onSubmit: signIn)

// Multi-line is the vertical axis with a line limit range.
TanreiTextField("Message", text: $message, axis: .vertical, lineLimit: 3...8)
import androidx.compose.foundation.text.KeyboardActions
import io.github.chnotchy.tanrei.components.TanreiTextField
import io.github.chnotchy.tanrei.components.TanreiTextFieldType

var email by remember { mutableStateOf("") }
TanreiTextField(
    value = email,
    onValueChange = { email = it },
    label = "Email",
    type = TanreiTextFieldType.Email,
    contentType = "email", // an HTML autofill name
    supportingText = "We only use it to sign you in.",
    isError = submitted && !isValid(email),
    errorText = "Enter a complete email address.",
    onCommit = { save(it) },
    keyboardActions = KeyboardActions(onDone = { signIn() }),
)

// singleLine is the inverse of the spec's multiline.
TanreiTextField(value = message, onValueChange = { message = it }, label = "Message", singleLine = false, minLines = 3)
import 'package:tanrei/tanrei.dart';

TanreiTextField(
  controller: email, // or initialValue: '…'
  labelText: 'Email',
  type: TanreiTextFieldType.email,
  autofillHints: TanreiTextField.autofillHintsFromHtml('email'),
  helperText: 'We only use it to sign you in.',
  isError: submitted && !isValid(email.text),
  errorText: 'Enter a complete email address.',
  onCommit: save,
  onSubmitted: (String value) => signIn(),
);

TanreiTextField(labelText: 'Message', multiline: true, minLines: 3);
import { TextField } from '@tanrei/react-native';

<TextField
  label="Email"
  type="email"
  autoComplete="email"
  value={email}
  onChangeText={setEmail}
  supportingText="We only use it to sign you in."
  error={submitted && !isValid(email)}
  errorText="Enter a complete email address."
  onEndEditing={save}
  onSubmitEditing={signIn}
/>
<TextField label="Message" multiline minRows={3} value={message} onChangeText={setMessage} />

One level down

The same field, written by hand from its two members, is the same picture, and to assistive technology the same text box with the same name, description and states:

<tanrei-field label="Email" required required-text="Required"
  supporting-text="We only use it to sign you in.">
  <tanrei-text-input name="email" type="email" autocomplete="email"></tanrei-text-input>
</tanrei-field>

The same field written by hand: a field with a text input in it

Go one level down when you need an arrangement the text field does not offer:

You need Use
A label, the box, help, an error and a count The text field
Something of your own inside the frame, beside or below the box A field with a text input and your own element in it
The box alone, under a label that stands elsewhere: a filter in a toolbar, a cell of a table, a row with its label beside it The text input alone, named by its own label

What changes one level down is where things are said. name and value are on the text input, which is then the control the form meets, and its events are heard on it. label, error, disabled and the requirement stay on the field, which passes them to the control: do not set the input’s own.

One thing the text field does that the field and the text input written by hand do not do: it keeps the count in step. By hand, with counter set, give the field the input’s count at every change:

field.countText = input.countText;
input.addEventListener('input', () => (field.countText = input.countText));

Specs

Error, disabled, read-only, affixes and counter

Types: password, search, number, telephone and URL

search

Required and optional: the tags, and the asterisk

Several lines: following the text, resized by a handle (down, and both ways), and fixed

Code: a pattern on one line, JSON on several lines, and lines that do not wrap

Code in a right-to-left page: the label and the help follow the page, the code begins at the left

A date, a time, and a date and time, in segments. Tab, the arrow keys and digits fill them

Filled and outlined, the Material presets' variants

Under m3-like

What each member gives

Aspect From the field From the text input
Properties label, labelHidden, supportingText, errorText, error, disabled, required, optional, requiredMark, requiredText, optionalText Every other one: variant, size, type, value, placeholder, readOnly, the lines and the resize, wrap, maxLength, counter, the prefix and the suffix, autocomplete, and everything of a date or a time
Events — input, change, action, pickerRequest, entryError
Slots label-icon leading-icon, trailing-icon
Tokens comp.field.* comp.input-box.* for the box, which every input box reads; comp.text-input.* for what a text input alone draws

The text input has a label, a labelHidden, an error and a disabled of its own. They are not what the text field sets: its label is the field’s, which names the input and, in a filled or an outlined field, is what floats in the box.

Tokens

The text field has no token of its own. Its box reads the text input’s tokens and its rows — the label, the mark, the help, the error and the count — read the field’s tokens, wherever they are set. The table below lists the field’s. To change one instance, set a member’s token on it:

<tanrei-text-field label="Email"
  style="--tanrei-comp-field-label-gap: 12px"></tanrei-text-field>

Anatomy

Anatomy

Drawn over the fixture outlined-empty, as the web package draws it.
Part Description
1 field The Field: the label row with its glyph and its requirement mark, and the supporting row with the help, the error text and the count the text field gives it (TXF-67). In a look whose input floats the label it draws no label row.
2 text-input The text input, as the Field's one control: the box and everything typed in it, the icons, the prefix and the suffix, the segments of a date, the picker button, the resize handle, and the label that floats in the box in the looks that float one.

Properties

Property Type Default Description
variant stacked | filled | outlined "stacked" Stacked is a filled box that shows focus and error with a line inside its edge and draws no label: the label of the Field that frames it stands above it. Filled and outlined float the label inside the box, marked by a tinted box and a bottom line, or by a border. Use one style per form. A design system may leave some out (the Tanrei system ships stacked only) or add values. — Text input
size small | medium | large "medium" Minimum height of a single-line input's container: stacked 32 / 40 / 56, as tall as a Button of the same size, so that an input and a Button side by side in one row stand as tall as each other; filled and outlined 40 / 48 / 56, which hold the label that floats inside them. Medium and large are for forms; small is for an input in an app bar, a toolbar or a dense row of controls, beside a small select or button. The hit area always meets the minimum target. — Text input
type text | email | password | number | search | tel | url | code | date | time | date-time "text" What the value is: chooses the keyboard, autocorrection, obscured entry and semantics. code is text written for a machine rather than for a reader — a snippet of configuration, a pattern, an identifier, a query, a key: a left-to-right run in the code face that the platform does not correct, capitalise, check for spelling or complete, on one line or, with multiline, on many. date, time and date-time are entered segment by segment in the locale's numeric form and held as ISO 8601. The value is a string for every type. — Text input
value string "" The text in the field; for date, time and date-time, the ISO 8601 form of what its segments give (2026-09-27, 14:30, 2026-09-27T14:30), or empty while a segment is empty or the value is not available. — Text input
label string "" Names the control, visibly and to assistive technology. Required, unless the control names itself (a checkbox or a switch with its own label): then leave it empty and the field adds only help and an error. — Field Names the text input. In a filled or outlined look the input draws it, floating in its box, and the Field draws no label row (FLD-25, TXI-13).
labelHidden boolean false Does not draw the label, which stays the accessible name. For a control whose visible label is elsewhere (a table column, a toolbar). — Field Hides the label wherever it would be drawn: the Field's row, or the input's floating label (FLD-25).
placeholder string "" A hint shown while the value is empty and the label is not in the input line (above, floated or hidden). Never a replacement for label. In a code field it is an example of the code and is drawn as code (TXI-53, TXI-54); a hint in words belongs in the supportingText of its Field. Ignored by a date, time or date-time field, whose empty segments each show their own. — Text input
supportingText string "" Help shown below the control and given to assistive technology as the control's description. While the field is in error it gives way to errorText, unless the design system keeps both (comp.field.error-text.placement append). — Field
errorText string "" Shown while error is true, in supportingText's place — or below it, which then stays, where the design system's comp.field.error-text.placement is append: what is wrong and how to put it right, in words. — Field
error boolean false Puts the field in the error condition: the error text is drawn, the control is exposed as invalid and, where it has an error condition of its own, drawn in it. What a value means is the product's to validate; the field never sets this itself. — Field The Field puts its text input in the input's own error condition (FLD-8, TXI-11).
disabled boolean false Disables the control and draws the frame as disabled. Disable a framed control here, not on the control, so that its label follows. — Field The Field disables its text input (FLD-14, TXI-5); the text field is then left out of its form (TXF-63).
readOnly boolean false The value can be focused, selected and copied, but not changed. — Text input
required boolean false What the form asks of the control: true exposes it as required and draws the required mark beside the label. It never validates: a missing answer is the product's to report with error. — Field The Field exposes its text input as required (FLD-10, TXI-23).
optional boolean false Draws the optional tag after the label and adds optionalText to the control's description, because assistive technology has no state for it. Ignored while required is true: a control is never both. — Field
requiredMark auto | tag | asterisk | none "auto" How this control shows that it is required: tag is a small filled Tag with requiredText, asterisk is "*" after the label, none draws nothing and keeps the semantics. Auto takes comp.field.requirement.mark, which follows sys.behavior.requirement-mark, the system's one answer. Ignored unless required is true. — Field
requiredText string "" The text of the required tag, in the product's language ("Required", "必須"). The product's: Tanrei has no word for it. Empty reads the product's words; with neither, FLD-24 says what is drawn. — Field
optionalText string "" The text of the optional tag, in the product's language ("Optional", "任意"); also read to assistive technology as part of the description. The product's: Tanrei has no word for it. Empty reads the product's words. — Field
multiline boolean false Accepts line breaks and shows several lines, sized as resize says. Ignored for password, date, time and date-time. — Text input
minRows number 2 The fewest lines a multi-line field shows: the height of an empty auto-resized field, the starting and smallest height of a manually resized one, and the height of a fixed one. — Text input
maxRows number 0 The most lines an auto-resized field grows to before it scrolls, and the tallest a manually resized one can be made; 0 is no limit. Ignored while resize is fixed. — Text input
resize auto | manual | fixed "auto" How a multi-line field gets its height. auto: it follows the number of lines between minRows and maxRows, with an animated change, and has no handle. manual: it starts at minRows lines and the person resizes it with the handle in its bottom end corner. fixed: it stays at minRows lines, with no handle. Ignored unless multiline. — Text input
resizeAxis vertical | both "vertical" What the person can resize by hand: the height only, or the height and the width. Ignored unless resize is manual. — Text input
resizeLabel string "" The accessible name of the resize handle, in the product's language ("Resize"). The product's: Tanrei has no word for it. Empty reads the product's words. Required while the handle is shown. — Text input
wrap boolean true Whether a multi-line code field continues a line that does not fit on following rows (true), or keeps every line of the code on one row and scrolls sideways inside its container (false) — for code whose lines are read as rows: a table of values, a log, a file in which a line break means something. The value is the same either way. Ignored unless type is code and multiline is true: prose always wraps, and a single line always scrolls in its line. — Text input
maxLength number 0 The most user-perceived characters the user can enter; 0 is no limit. Ignored for date, time and date-time. — Text input
counter boolean false Keeps the character count (and maxLength, when set): part of the input's description and announced when typing pauses (TXI-19). The input draws none: a text field gives the count to its Field, which draws it at the end of its supporting row, and a product that frames the input by hand gives it to the Field's countText. — Text input The input keeps the count, the text field gives it to its Field, and the Field draws it (TXI-19, TXF-67, FLD-27).
counterText string "" The count said to assistive technology while counter is true and maxLength is greater than 0: the whole sentence in the product's language with three places the input fills — {count} with the characters entered, {limit} with maxLength and {remaining} with how many more can be entered ("{remaining} characters left", "{limit}文字中{count}文字"). Empty reads the product's words; with neither, the count is said as the digits it is drawn in ("12/40"). A sentence that is right for every number, or a prop set per count where the language needs another form for one. — Text input
counterNoLimitText string "" The count said to assistive technology while counter is true and maxLength is 0: the whole sentence with one place, {count}, filled with the characters entered ("{count} characters", "{count}文字"). Empty reads the product's words; with neither, the count is said as its digits. — Text input
prefixText string "" Fixed text before the value, not part of it. In a code field it is code: drawn in the code face at the left of the value in every layout direction (TXI-53). — Text input
suffixText string "" Fixed text after the value, not part of it. In a code field it is code: drawn in the code face at the right of the value in every layout direction (TXI-53). — Text input
autocomplete string "" The autofill hint, as an HTML autofill field name (email, current-password, new-password, one-time-code, tel, postal-code, …); each platform maps it to its own content type. A code field left without one asks the platform to offer no remembered entry (TXI-55). — Text input
locale string "" A BCP 47 language tag ("en-US", "ja-JP") for how a date, time or date-time field shows and reads its value: the order of the year, month and day segments, the separators, the clock, the digits and the names of the units. Empty uses the platform's current locale. The calendar is the Gregorian calendar in every locale. — Text input
min string "" The earliest value a date, time or date-time field accepts from the person, in its ISO 8601 form; empty for no limit. — Text input
max string "" The latest value a date, time or date-time field accepts from the person, in its ISO 8601 form; empty for no limit. — Text input
disabledDates strings Dates inside min and max that a date or date-time field does not accept from the person, as ISO 8601 calendar dates — the same list the product gives its DateTimePicker. Ignored by a time field. — Text input
invalidText string "" The words for a date, time or date-time input committed with some segments filled and others empty: given to the Field that frames it, which shows them as its error text (TXI-30). — Text input Shown by the Field, in its supporting row (FLD-28).
unavailableText string "" The words for a date, time or date-time input committed with a value before min, after max or, for a date, listed in disabledDates: given to the Field that frames it, which shows them as its error text (TXI-30). — Text input Shown by the Field, in its supporting row (FLD-28).
segmentEmptyText string "" What assistive technology reads as the value of a date or time segment that holds nothing yet, in the product's language ("Empty"). The product's: Tanrei has no word for it. Empty reads the product's words; with neither, an empty segment has no value text. — Text input
picker boolean false Shows the picker button in a date, time or date-time field. The field presents no picker itself: it emits pickerRequest, and the product presents a DateTimePicker in the matching mode and sets value to what was picked. — Text input
pickerLabel string "" The accessible name of the picker button ("Choose date"). Required while picker is shown. — Text input

Slots

Slot Description
leading-icon An icon at the start of the input. — Text input
trailing-icon An icon at the end of the input, or the product's own icon-only Button (clear, reveal password). A date or time field's picker button comes after it. — Text input
label-icon A glyph before the label. — Field

Events

Event Description
input The user changed the value; carries the new value. Emitted for every change, not when the product sets value. A date, time or date-time field emits it when its value changes — when its last segment is filled, a filled field is edited, or it is emptied — not for a segment that leaves the value empty. — Text input
change The user committed a changed value (focus left the field, or the action key was pressed in a single-line field); carries the value. — Text input
action The user pressed the keyboard's action key (Enter, Return, Go, Search, Done) in a single-line field. — Text input
pickerRequest The user activated the picker button of a date, time or date-time field; carries the value, type, min and max. The product presents the picker and sets value to the picked value. — Text input
entryError The user committed a date, time or date-time field that is half-filled or whose value is not available; carries the reason (invalid: some segments are empty; unavailable: before min, after max or a listed date) and the text of the row of segments as drawn. The entry error itself is shown by the Field that frames the input (TXI-30). — Text input
resizeEnd The person finished changing the size of a field resized by hand: a drag of its handle ended, or a key or an activation of the handle changed the size (TXI-69). Carries lines, the number of lines the input shows, and width, the field's width in pixels WHILE resizeAxis is both (null otherwise). Not emitted when a change of resize, resizeAxis, minRows or maxRows gives the size back. — Text input

States

Behavior

Requirements in EARS form. Every platform implementation is tested against these ids.

ID Requirement
TXF-62 THE text field SHALL be a Field that frames one text input, and nothing else. It SHALL give label, labelHidden, supportingText, errorText, error, disabled, required, optional, requiredMark, requiredText and optionalText to its Field and every other prop to its text input, each unchanged; SHALL hold the label-icon slot for its Field and the leading-icon and trailing-icon slots for its text input; and SHALL emit its text input's events as its own, each once. THE text field SHALL be as wide as the place it stands in, and its text input SHALL fill it: it is a block with no width of its own, as its Field is (FLD-1), around a control that fills the width it is given (TXI-63). It SHALL draw no part of its own, have no token of its own and hold no state of its own but the count it carries from one member to the other (TXF-67): what it shows, how it behaves and what it exposes to assistive technology SHALL be exactly what that Field and that text input do when a product places the one in the other (FLD-1 to FLD-28; TXI-1 to TXI-63), with the Field given the input's count — the label row and its requirement mark (FLD-2 to FLD-6), or the label that floats in the box with the Field's row left out (FLD-25, TXI-13, TXI-50); the supporting row, the error and the count (FLD-7 to FLD-9, FLD-26, FLD-27, TXI-19, TXF-67); the name, the description and the states (FLD-10, FLD-11, FLD-14, TXI-23); and a date's entry error shown in the Field's row (FLD-28, TXI-30).
TXF-63 WHERE the platform has forms (the web) THE text field SHALL be the one control the form sees: it SHALL take name, submit its text input's value, reset to the value it was first given, restore it on navigation, report to the form's constraint validation exactly what its text input would report there (TXI-23, TXI-30), and be left out WHILE disabled is true or a disabled group of the form holds it; its text input and its Field SHALL take no part in the form themselves. WHEN the action key asks for the form's submission (TXI-3) the form asked SHALL be the text field's own.
TXF-64 WHEN the text field is given focus — by the product, by a press on any part of it that FLD-12 or TXI-8 gives to the input, or by a form that reports its validity — THE text field SHALL pass the focus to its text input (in a date, time or date-time field, to the segment TXI-43 names) and SHALL add no stop to the focus order: the stops inside it are its text input's, in that input's order (an interactive slot element, the picker button, the resize handle). WHILE focus is anywhere inside it THE text field SHALL count as focused for its Field (FLD-13).
TXF-65 IF the text field is placed in a Field THE text field SHALL in debug builds report that it is a field already and that the bare control is the text input, and both SHALL still render (FLD-15). WHILE it stands in a field group THE text field SHALL follow that group's disabled, through its own Field, and take nothing else from the group (FGR-20).
TXF-66 THE text field SHALL have no words of its own and SHALL read none: requiredText and optionalText are read by its Field (FLD-24), resizeLabel and segmentEmptyText by its text input (TXI-52), each from the instance's prop and then from the product's words.
TXF-67 WHILE counter is true THE text field SHALL give its Field, as its count text (FLD-27), the count its text input keeps (TXI-19), as the input writes it, and SHALL give the new one in the same frame in which the value changes — by typing, by a paste, because the product set value or maxLength — without a motion and without an announcement of its own: the input describes and announces its count itself (TXI-19) and the Field keeps the drawn text from assistive technology (FLD-27). WHILE counter is false THE text field SHALL give its Field no count text. THE text field SHALL NOT count, format or write a digit itself.
TXF-68 WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE text field SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the text field is invalid — except those the text field owns (aria-label, aria-labelledby, aria-disabled), which it SHALL move and not expose: its name is label (TXF-62). WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE text field SHALL take it off that element. WHEN the product sets an aria- attribute the text field owns THE text field SHALL report it in debug builds.

Platform name mapping

Parity does not mean identical APIs. Each platform uses its own idiom for the same contract.

Spec Web SwiftUI Jetpack Compose Flutter React Native
Component tanrei-text-field TanreiTextField TanreiTextField TanreiTextField TextField
variant · Property variant variant variant variant variant
size · Property size size size size size
type · Property type type type type type
value · Property value text value controller value
label · Property label label label labelText label
labelHidden · Property labelHidden labelsHidden labelHidden labelHidden labelHidden
placeholder · Property placeholder prompt placeholder hintText placeholder
supportingText · Property supportingText supportingText supportingText helperText supportingText
errorText · Property errorText errorText errorText errorText errorText
error · Property error error isError isError error
disabled · Property disabled disabled enabled enabled disabled
readOnly · Property readOnly readOnly readOnly readOnly readOnly
required · Property required isRequired required required required
optional · Property optional isOptional optional optional optional
requiredMark · Property requiredMark requiredMark requiredMark requiredMark requiredMark
requiredText · Property requiredText requiredText requiredText requiredText requiredText
optionalText · Property optionalText optionalText optionalText optionalText optionalText
multiline · Property multiline axis singleLine multiline multiline
minRows · Property minRows lineLimit minLines minLines minRows
maxRows · Property maxRows lineLimit maxLines maxLines maxRows
resize · Property resize resize resize resize resize
resizeAxis · Property resizeAxis resizeAxis resizeAxis resizeAxis resizeAxis
resizeLabel · Property resizeLabel resizeLabel resizeContentDescription resizeSemanticLabel resizeAccessibilityLabel
wrap · Property noWrap wrap softWrap softWrap wrap
maxLength · Property maxLength maxLength maxLength maxLength maxLength
counter · Property counter counter counter showCounter counter
counterText · Property counterText counterText counterText counterSemanticText counterAccessibilityText
counterNoLimitText · Property counterNoLimitText counterNoLimitText counterNoLimitText counterNoLimitSemanticText counterNoLimitAccessibilityText
prefixText · Property prefixText prefix prefix prefixText prefix
suffixText · Property suffixText suffix suffix suffixText suffix
autocomplete · Property autocomplete textContentType contentType autofillHints autoComplete
locale · Property locale locale locale locale locale
min · Property min min min min min
max · Property max max max max max
disabledDates · Property disabledDates disabledDates disabledDates disabledDates disabledDates
invalidText · Property invalidText invalidText invalidText invalidText invalidText
unavailableText · Property unavailableText unavailableText unavailableText unavailableText unavailableText
segmentEmptyText · Property segmentEmptyText segmentEmptyText segmentEmptyText segmentEmptyText segmentEmptyText
picker · Property picker onPickerRequest onPickerRequest onPickerRequest onPickerRequest
pickerLabel · Property pickerLabel pickerLabel pickerContentDescription pickerSemanticLabel pickerAccessibilityLabel
input · Event input text onValueChange onChanged onChangeText
change · Event change onCommit onCommit onCommit onEndEditing
action · Event action onSubmit keyboardActions onSubmitted onSubmitEditing
pickerRequest · Event pickerrequest onPickerRequest onPickerRequest onPickerRequest onPickerRequest
entryError · Event entryerror onEntryError onEntryError onEntryError onEntryError
resizeEnd · Event resizeend onResizeEnd onResizeEnd onResizeEnd onResizeEnd
Notes The tag is tanrei-text-field, the class TanreiTextField, the React name TextField. Members: <tanrei-field> holding one <tanrei-text-input> in its default slot; the product places none. The host is display: block, as wide as its place, and the input fills it (TXF-62). Value member: tanrei-text-input. The host is the form's element — name, the value and its state, validity anchored on the input, reset to the value attribute, restore, form, a disabled fieldset — and the input inside has no name. Props, set as properties on the member each is from. On the Field: label, labelHidden, supportingText, errorText, error, disabled, required, optional, requiredMark, requiredText, optionalText. On the text input: variant, size, type, value, placeholder, readOnly, multiline, minRows, maxRows, resize, resizeAxis, resizeLabel, noWrap (the inverse of wrap, attribute no-wrap), maxLength, counter, prefixText, suffixText, autocomplete, locale, min, max, disabledDates, invalidText, unavailableText, segmentEmptyText, picker, pickerLabel. Attributes are the members' own: kebab-case, except readonly, maxlength, autocomplete, min and max. Props of one name on both members — label, labelHidden, error, disabled — are the Field's; the input's are never set by the element and reach the input through the connection. Events: input, change, action, pickerrequest, entryerror are the text input's own composed events and leave by themselves, with the host as their target and value already new; action stays cancelable. None is stopped and none is copied. Slots: label-icon is passed to the Field, leading-icon and trailing-icon to the text input, each by a slot element of the same name rendered only while the host has a child for it; the product's own Button in trailing-icon stays the product's element. Parts: the member hosts are field and text-input. Exported from the Field: label, label-icon, requirement-label, supporting-text, counter. Exported from the text input: container, outline, active-indicator, label, requirement-label, input, segment, prefix, suffix, leading-icon, trailing-icon, picker-button, resize-handle. Two names collide, label and requirement-label. Each is exported with its member's name before it, always (field-label, text-input-label, field-requirement-label, text-input-requirement-label), and under the bare name from both members as well, because the two are one thing to a person and are never drawn at once: FLD-25 is the sentence that holds it — while its control draws the label itself the Field draws no label row and no requirement tag, and a stacked input draws neither (TXI-13, TXI-50). So tanrei-text-field::part(label) keeps meaning the drawn label. Focus: the shadow root does not delegate. focus() and blur() call the text input's; a press on the label row — the label, its glyph, the requirement mark — focuses the input by FLD-12, the Field's own sentence (the Field calls focus() on its control, which stands beside it in the same tree); a press on the box, and on a label that floats in it, by TXI-8; and a press on the supporting row — the help, the error text, the count — or on the gap between a row and the box does nothing (FLD-12). The connection is the Field's with its control, in both directions (FLD-25, FLD-26, FLD-28, TXI-62); the element takes no part in it. A member's prop it sets itself (fills): the Field's countText, from the text input's read-only countText after each of the input's updates, in the same task (TXF-67). Parts and tokens of its own: none. The element writes no custom property: --tanrei-comp-field-*, --tanrei-comp-input-box-* and --tanrei-comp-text-input-* reach the members by inheritance, and one text field is set apart by setting any of them on the element. What its twin cannot do that it does: keep the Field's count text equal to the input's count as the person types (TXF-67); nothing else. The twin: <tanrei-field label="Email" supporting-text="We never share your address." required><tanrei-text-input name="email" type="email" autocomplete="email"></tanrei-text-input></tanrei-field>. Its Field draws the help and the error text together (FLD-7, FLD-31), and the five fixtures *-supporting-error* hold the pair to its twin. Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: text: Binding<String> (as TextField); every edit writes the binding, which is the input event. password draws a SecureField; multiline is TextField(axis: .vertical) with .lineLimit(minRows...maxRows) — not TextEditor, which has no prompt and does not grow. labelHidden follows the .labelsHidden() environment; disabled follows .disabled(_:); action is the onSubmit: parameter, called after the commit (TXI-3); an enclosing .onSubmit modifier still fires, but before the commit, because SwiftUI runs the outer handler first. onCommit is Tanrei's own parameter (SwiftUI deprecated its own), called when focus leaves with a changed value. type maps to .keyboardType, .textInputAutocapitalization, .autocorrectionDisabled and .submitLabel; autocomplete maps HTML names to UITextContentType. locale follows the \.locale environment when not given. The picker button is shown when an onPickerRequest: closure is given (a nil closure is picker false); the product presents the picker and writes the binding. Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from the \.tanreiWords environment value. Not built on this platform yet, for type code: .autocorrectionDisabled(), .textInputAutocapitalization(.never), .keyboardType(.asciiCapable), a monospaced design of the input's size, and .environment(\.layoutDirection, .leftToRight) on the input row alone; smart quotes, smart dashes and smart insert and delete are traits of the UIKit and AppKit text view (smartQuotesType, smartDashesType, smartInsertDeleteType, isAutomaticQuoteSubstitutionEnabled), so a code field is drawn on that view where SwiftUI's TextField gives no access to them; wrap: false needs that view too (a text container that does not track the view's width, in a scroll view), because TextField(axis: .vertical) always wraps. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement — the error text takes the help's place under every system, which is replace, the default; append, which keeps the help and draws the error text below it, is not drawn (FLD-7, FLD-31). Not built on this platform yet: size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67). Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: Compose idiom (as TextField): value / onValueChange, with a TextFieldValue overload for products that manage selection and composition themselves; enabled and singleLine are inverse flags; isError, readOnly, minLines, maxLines, prefix and suffix keep Compose's names. Built on BasicTextField. type maps to KeyboardOptions (keyboard type, capitalization, autocorrect, IME action) and, for password, PasswordVisualTransformation; autocomplete maps to ContentType semantics. onCommit is Tanrei's own callback, called when focus leaves with a changed value or the IME action commits. locale defaults to the current configuration's locale. The picker button is shown when onPickerRequest is not null (null is picker false); pickerContentDescription follows Compose's naming for an icon's accessible name. Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from LocalTanreiWords. Not built on this platform yet, for type code: KeyboardOptions(capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false, keyboardType = KeyboardType.Ascii), TextStyle(textDirection = TextDirection.Ltr, textAlign = TextAlign.Left, fontFamily = …, fontFeatureSettings = "liga 0") on the input row inside CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr); softWrap = false puts the BasicTextField in a horizontalScroll that follows the cursor's rectangle. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement — the error text takes the help's place under every system, which is replace, the default; append, which keeps the help and draws the error text below it, is not drawn (FLD-7, FLD-31). Not built on this platform yet: size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67). Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: Flutter idiom (as TextField and InputDecoration): the value lives in a TextEditingController (or an internal one with initialValue); labelText, hintText, helperText, errorText, prefixText, suffixText, minLines, maxLines, maxLength, readOnly, enabled and autofillHints keep Flutter's names, enabled being the inverse flag. isRequired and showCounter avoid Dart's required keyword and Flutter's own counter, which Tanrei replaces (it counts grapheme clusters with characters). maxRows: 0 is maxLines: null. type maps to keyboardType, obscureText, autocorrect, textCapitalization and textInputAction. onCommit is Tanrei's own callback, called when focus leaves with a changed value. locale defaults to Localizations.localeOf(context). The picker button is shown when onPickerRequest is not null (null is picker false). Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from the TanreiWords inherited widget, which the app fills from its own localizations. Not built on this platform yet, for type code: autocorrect: false, enableSuggestions: false, textCapitalization: TextCapitalization.none, smartDashesType: SmartDashesType.disabled, smartQuotesType: SmartQuotesType.disabled, spellCheckConfiguration: const SpellCheckConfiguration.disabled(), textDirection: TextDirection.ltr, textAlign: TextAlign.left, a TextStyle with the code family and fontFeatures: [FontFeature.disable('liga')], the input row in a Directionality(textDirection: TextDirection.ltr); softWrap: false is an EditableText of unbounded width in a horizontal Scrollable that keeps the caret in view, because a multi-line TextField always wraps. Not built on this platform yet: helperText stays while the field is in error and errorText is drawn below it (FLD-7, FLD-31), unlike InputDecoration; today the error text takes the help's place. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement (FLD-7, FLD-31), size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67). Lagging: this platform draws the text field as one view that draws its own frame, written against the token group comp.text-field, which the tokens do not have. It owes, first, its sources moved to the generated names of comp.input-box, comp.text-input and comp.field: they do not compile until then. Then the split: the text input as a view of its own, and the text field as the Field around it. Its names and its props stay. What it does today: React Native idiom (as TextInput): value / onChangeText, onEndEditing for a commit, onSubmitEditing for the action key; multiline, readOnly, maxLength and autoComplete keep TextInput's names. type maps to keyboardType, secureTextEntry, autoCapitalize, autoCorrect, inputMode and returnKeyType. A controlled value echoed back during a composition must not reset it (TXI-4). React Native gives JavaScript no signal that a composition is in progress, so the component cannot defer a write the product makes mid-composition; echoing the value back is safe. For date, time and date-time, onChangeText receives the value (ISO 8601 or empty), not the text as typed (TXI-29). The picker button is shown when onPickerRequest is given (undefined is picker false). Locale data needs Intl with formatToParts, which Hermes has only in part. Not built on this platform yet: the optional flag (optional) beside the required flag; the resize modes and the handle (TXI-37–TXI-41, TXI-69), the segmented date and time row (TXI-42–TXI-47) and the requirement label (FLD-4, FLD-5, TXI-50). The platform proposes its own idiom for the segments when it catches up — each unit focusable and adjustable on its own, with the same value and events. Not built on this platform yet: the words props have no defaults, and the product's words are read from the TanreiWordsProvider context. Not built on this platform yet, for type code: autoCorrect={false}, autoCapitalize="none", spellCheck={false}, autoComplete="off", smartInsertDelete={false}, keyboardType="ascii-capable" on iOS and "visible-password" on Android only where suggestions cannot be turned off otherwise, style={{ writingDirection: 'ltr', textAlign: 'left', fontFamily: … }}, the input row with direction: 'ltr'. TextInput has no multi-line form that does not wrap: wrap={false} needs a native text view, and until one exists the field wraps and its README says so. Not built on this platform yet: the count said as the product's sentence (counterText, counterNoLimitText, TXI-19); today the digits are said. Not built on this platform yet: comp.field.error-text.placement — the error text takes the help's place under every system, which is replace, the default; append, which keeps the help and draws the error text below it, is not drawn (FLD-7, FLD-31). Not built on this platform yet: size small (40), and in a right-to-left layout the value of an always left-to-right type beside its prefix (TXI-67).

Tokens

Token Aliases Resolved (light)
--tanrei-comp-field-counter-gap sys.space.space200 16px
--tanrei-comp-field-error-text-gap sys.space.space50 4px
--tanrei-comp-field-error-text-placement — 0
--tanrei-comp-field-label-icon-gap sys.space.space100 8px
--tanrei-comp-field-label-icon-size sys.icon.small 20px
--tanrei-comp-field-label-gap sys.space.space100 8px
--tanrei-comp-field-motion-state-duration sys.motion.role.state.default 250ms
--tanrei-comp-field-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-field-requirement-asterisk-gap sys.space.space25 2px
--tanrei-comp-field-requirement-gap sys.space.space100 8px
--tanrei-comp-field-requirement-mark sys.behavior.requirement-mark 0
--tanrei-comp-field-standard-counter-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-standard-disabled-counter-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-error-text-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-error-counter-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-error-label-icon-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-error-label-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-error-required-asterisk-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-error-supporting-text-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-error-value-text-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-label-icon-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-required-asterisk-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-supporting-text-color — #1b1b1b · 38%
--tanrei-comp-field-standard-disabled-value-text-color — #1b1b1b · 38%
--tanrei-comp-field-standard-error-text-color sys.color.error #cd262f
--tanrei-comp-field-standard-error-counter-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-standard-error-label-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-standard-error-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-field-standard-error-required-asterisk-color sys.color.primary #1b1b1b
--tanrei-comp-field-standard-error-supporting-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-standard-error-value-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-standard-focused-counter-color — #4b4b4b
--tanrei-comp-field-standard-focused-error-text-color — #cd262f
--tanrei-comp-field-standard-focused-error-counter-color — #4b4b4b
--tanrei-comp-field-standard-focused-error-label-icon-color — #4b4b4b
--tanrei-comp-field-standard-focused-error-label-color — #1b1b1b
--tanrei-comp-field-standard-focused-error-required-asterisk-color — #1b1b1b
--tanrei-comp-field-standard-focused-error-supporting-text-color — #4b4b4b
--tanrei-comp-field-standard-focused-error-value-text-color — #4b4b4b
--tanrei-comp-field-standard-focused-label-icon-color — #4b4b4b
--tanrei-comp-field-standard-focused-label-color — #1b1b1b
--tanrei-comp-field-standard-focused-required-asterisk-color — #1b1b1b
--tanrei-comp-field-standard-focused-supporting-text-color — #4b4b4b
--tanrei-comp-field-standard-focused-value-text-color — #4b4b4b
--tanrei-comp-field-standard-label-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-standard-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-field-standard-required-asterisk-color sys.color.primary #1b1b1b
--tanrei-comp-field-standard-supporting-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-standard-value-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-field-supporting-text-gap sys.space.space50 4px
--tanrei-comp-field-value-text-gap sys.space.space200 16px
--tanrei-comp-field-label-typescale sys.typescale.label 500 14px/1.4286
--tanrei-comp-field-supporting-text-typescale sys.typescale.label-small 500 12px/1.3333
--tanrei-comp-field-value-text-typescale sys.typescale.label 500 14px/1.4286

Fixtures

Canonical renderings. Every platform snapshots each fixture under the same id so the results can be compared.

Generated from specs/text-field.spec.json, tokens/comp/field.tokens.json, design/tokens/

What the web draws differently

  • The element’s shadow root holds a <tanrei-field> with a <tanrei-text-input> in it, and nothing else. Importing the element defines both.
  • The element is a block that fills its place, not an inline box as wide as the browser’s own text input.
  • The element is form-associated. It takes name and form, submits the value — nothing while disabled —, resets to the value attribute, is restored on navigation, follows a disabled <fieldset>, and reports a missing value while required is set and it is empty. Its two members add no entry to the form. Enter in a single-line field presses the default button of the element’s form; cancel action to prevent it.
  • input, change, action, pickerrequest and entryerror are the text input’s own events. Their target is the element, and event.target.value is the new value in every listener. The detail types are the input’s: TanreiTextInputActionDetail, TanreiTextInputPickerRequestDetail and TanreiTextInputEntryErrorDetail.
  • Parts: field and text-input are the two members. Their own parts have the same names here as one level down: label-icon, supporting-text, counter; container, outline, active-indicator, input, segment, prefix, suffix, leading-icon, trailing-icon, picker-button, resize-handle. label and requirement-label are the drawn ones, whichever member draws them; field-label, text-input-label, field-requirement-label and text-input-requirement-label name one member’s.
  • The element is not focusable itself and has no role: focus() goes to the input. A press on the label row, on the box or on a floating label focuses the input. A press on the help, the error text, the count or the gap between a row and the box does nothing.
  • Name it with label — with label-hidden where the label is not to be drawn — and never with aria-label, which names nothing here.
  • The attributes are the members’ own: kebab-case, such as supporting-text, error-text, min-rows and no-wrap, except readonly, maxlength, autocomplete, min and max. There is no required attribute: say required.
  • Before the element is defined it has no size. Reserve its height.

Guidelines

When to use

Use a text field when people type a value: a name, an email address, a search, a message. When they pick from a known set of values, use a control made for choosing. Where only the box is wanted, because the label stands elsewhere — a filter in a toolbar, a cell of a table — use a text input.

Choosing a variant and a size

Use one variant throughout a form.

Variant Marked by Where
Stacked (default) The label above a tinted box with no edge. Focus draws a line inside the box’s edge, which moves nothing; an error draws a thin line at rest. Every system. It is the only variant the Tanrei system ships.
Filled A tinted box and a bottom line, with the label floating inside The Material and cupertino presets
Outlined A border that thickens when focused, with the label floating into it The Material and cupertino presets

The default is stacked in every system, including the presets. A Material product that wants Material’s floating label sets variant to outlined or filled on its fields. Under the Tanrei system, filled and outlined still work, with a warning in development.

medium (40) is the dense field and large (56) the standard one. Use medium in filters and dense settings, and large in sign-in and checkout forms. small (32) is the small select’s height, for a field in an app bar, a toolbar or a dense row of controls, where its input text is 14. The input text is 16 at the other two sizes in the default system. Heights are minimums: the field grows with larger text, and the hit area always meets the minimum target.

Labels and placeholders

  • Every field needs a label. In a stacked field it stands above the box, from its start edge, and never moves or resizes; a long label wraps rather than being cut. A label-icon slot can put a glyph before it. In a filled or outlined field the label rests in the field while it is empty, and floats when the field is focused or filled.
  • labelHidden stops drawing the label, but it stays the accessible name, and the help and the error stay below the box. Where the box alone is wanted — a filter in a toolbar, a cell, a row whose label stands beside it — use a text input with its own label instead.
  • The placeholder is a hint, such as an example value. It shows while the field is empty and the label is not in the input line: always in a stacked field, and only while the label is floated or hidden in the others. It is never the name.

Required and optional

required and optional say what the form asks of a field: required that it must be answered, optional that it may be left out. A field is never both: with both set it is required.

  • Required exposes the field as required and draws the required mark beside the label.
  • Optional draws the requirement tag in its optional form and adds its text to the field’s description.
  • The mark is a small tag after a stacked label by default: filled for required, tonal for optional. Its words are requiredText and optionalText, and they are yours: on the field, or once for the whole product with setWords(). Tanrei ships none: without requiredText an asterisk stands in the tag’s place, and without optionalText no tag is drawn. It does not make the label’s row taller, and it wraps to a line of its own when the label leaves it no room.
  • An asterisk instead. Which mark a required field draws is one choice of the design system for every form, sys.behavior.requirement-mark: the tag in the default system, an asterisk in the Material presets, nothing in cupertino. requiredMark overrides it for one field (tag, asterisk, or none, which keeps the semantics and draws nothing). A filled or outlined field draws the asterisk where the tag is asked for.
  • Optional in a floating label. A filled or outlined field has no room for the optional tag beside a label that floats: it draws optionalText after the label instead, as a word (“Company Optional”, “会社名 (任意)”), resting and floated with it. Write the word as your forms show it, with or without brackets.
  • It validates nothing. The field never sets error for a missing value. You do, with error and errorText.

Mark the fewer of the two in a form: where most fields are required, mark the optional ones, and the other way round. If you use the asterisk — and a filled or outlined field always does for a required field — say once in the form, near its top, what it means (“* Required”): an asterisk is a convention, not a word, and not everyone knows it.

A required field shows a mark: set requiredMark to none where you do not want one.

Help and error text

  • supportingText is one line of help below the field. It is also read as the field’s description.
  • Validation is yours. When a value is wrong, set error and write errorText: it takes the supporting text’s place while the field is in error, so a form in error is no taller than at rest. Say in it what the help said, where that still matters. A design system that keeps the help in view sets comp.field.error-text.placement to append: the error text is then drawn below the help, and the field is taller by a line while it is in error (see the field).
  • Say how to fix the value, not only that it is wrong: “Enter a complete email address.”, not “Invalid”.
  • Show an error after the person leaves the field or submits the form, not while they are still typing.

The counter

  • maxLength limits what a person can type. It counts characters as people see them, so an emoji or an accented letter counts once. A paste is shortened to fit. A value you set is never shortened.
  • counter shows the count at the end of the supporting row, followed by the limit when maxLength is set (“12/40”).
  • Show the counter only when the limit matters to the person, such as a display name or a short bio.

Input types

type chooses the keyboard, autocorrection and obscured entry. The value is a string for every type.

Type Keyboard and behaviour
text A general keyboard.
email An email keyboard, no autocapitalization or autocorrection.
password Obscured, secure entry, no autocorrection. Always one line.
number A numeric keyboard with a decimal separator. The value is not parsed, rounded or validated.
search A general keyboard whose action key reads as search.
tel A phone keypad.
url A URL keyboard, no autocapitalization or autocorrection.
code A general keyboard, and nothing done to the text: no autocorrection, autocapitalization, spelling check or substituted characters. One line or, with multiline, several. See Code.
date, time, date-time A date, a time, or both, entered unit by unit in the locale’s numeric form. Always one line. See Dates and times.

Add autocomplete with an HTML autofill name (email, current-password, one-time-code …) so the platform can fill the field. Each platform maps it to its own content type. A short code of known length that is drawn in cells is a one-time-code input.

Several lines

multiline accepts line breaks and shows several lines. resize says how the field gets its height:

resize Height Handle minRows and maxRows
auto (default) Follows the number of lines, with a short motion None The fewest and the most lines. Past the most it scrolls; 0 is no limit
manual What the person gives it. It starts at minRows In the bottom corner at the end edge The smallest and the tallest it can be made
fixed minRows lines None maxRows is ignored
  • Auto suits most fields: a comment, a message, a description. Set maxRows where a long text must not push the rest of the form away.
  • Manual suits text a person reads back and arranges: notes, a long description, code. resizeAxis is vertical by default; both also lets the person change the field’s width, within what its layout allows. The size is kept, and the text wraps and scrolls inside it.
  • Fixed suits a form whose layout must not move, such as an address block.
  • The handle works without a drag. It is a handle of the small size in the bottom end corner, out of sight until a pointer is over the field or the field has focus, and a focus stop named by resizeLabel. The arrow keys resize by a line, Page Up and Page Down by minRows lines, Home and End go to the smallest and the tallest, and a click, Enter or Space fits the height to the text, or returns it to minRows when it already fits. Where resizeAxis is both, the same click also makes the field as wide as its place, and the next one gives back the width it had, so a pointer can change the width without dragging. Its target is the minimum target, though its line is small. When the person has changed the size, the field fires resizeend with the lines it shows (and its width, where it resizes both ways), so you can keep the size they chose. A disabled field has no handle; a read-only one keeps it, because reading may need the room.
  • Nothing above the text moves. A multi-line field’s first line stands where a single-line field draws its line, and its icons, prefix and suffix are centred on that line. Every further line lies below, so the label and the first line stay put while the height changes.

Code

type="code" is for text written for a machine rather than for a reader: a pattern, an identifier, a key, a query, a snippet of JSON or YAML. It is the same field — the same label, help, error, counter, sizes and form behaviour — with an input line that holds the code exactly as it is written.

  • Left to right, in every language. The value, its caret, the placeholder, the prefix and the suffix are one left-to-right row that begins at the left, also in a right-to-left page and whatever the code’s first letter is: an empty field, one that holds { alone and one whose first string is Arabic are laid out alike. Everything else follows the page: the label, the icons, the help or error text, the counter and the resize handle. A string or a comment in a right-to-left script is laid out inside its line and reorders nothing around it.
  • The code face, at the input’s size. The family is the code role’s; the size and the line height stay the input’s, so a code field is as tall as the text field beside it. Every space is kept, a tab character reaches the next tab stop, and no two characters are joined into a ligature. Nothing is colored by what it means.
  • Left alone. The platform is asked not to correct, capitalise, spell-check, translate or complete the value, and not to turn straight quotation marks into curved ones. A paste is plain text, character for character. The value is never trimmed, parsed or validated: whether it is well formed is yours to say, with error and errorText.
  • One line or several. One line suits a pattern or a key. With multiline, a line that does not fit continues on following rows by default; the value holds only the line breaks of the code.
  • Lines that do not wrap. wrap false — no-wrap on the web — keeps each line of the code on one line of the field, for a table of values, a log, or a file in which a line break means something. A long line scrolls sideways inside the field and widens nothing. Only a multi-line code field reads wrap: prose always wraps, and a single line always scrolls in its line.
  • Tab moves focus. No key changes meaning: Tab never indents, Enter adds no indent, and no bracket is closed for you. A tab character arrives by paste or from your product, and is kept.
  • Not an editor. There are no syntax colors, no line numbers, no folding and no error marks by line. For code that is shown and not typed, use a Code block; for an editor, embed the one your product has chosen.
  • A placeholder is an example of the code, drawn as code. A hint in words belongs in supportingText, which is also where the field says what it expects (“JSON”).

In a right-to-left field a code value begins at the left, while an email, url, tel, number or password value is left to right at the field’s start edge, the right. The other platforms have not built the code type yet.

Dates and times

A date, time or date-time field is a row of segments in the input line, one for each unit: the month, the day and the year, the hour, the minute and, on a 12-hour clock, the day period. They stand in the locale’s order with its separators. The field holds the value in ISO 8601, with no time zone.

Type Value Row in en-US Row in ja-JP
date 2026-09-27 09/27/2026 2026/09/27
time 14:30 2:30 PM 14:30
date-time 2026-09-27T14:30 09/27/2026, 2:30 PM 2026/09/27 14:30
  • The locale. locale, or the platform’s, sets the order, the separators, the clock and the digits. An empty segment shows its own placeholder (“mm”, “dd”, “yyyy”), so the row always shows its form. Each segment is as wide as the wider of its placeholder and its widest value, so the row does not move as you type or clear it. placeholder, prefixText, suffixText, maxLength and multiline are ignored by these types.
  • Moving. Tab visits each segment, then the picker button. Left and Right Arrow move to the segment on that side, in every layout direction: the row is written left to right, as a date in digits is. A press on a segment focuses it. The focused segment is drawn as selected text, with no caret.
  • Stepping. Up and Down Arrow change a segment by one and wrap; on an empty segment they start from today’s date or the current time. Page Up and Page Down step further, and Home and End go to the ends of the range.
  • Typing. A digit fills the segment, and focus moves on when no further digit could extend it, or when the separator is typed. Backspace removes a digit, and on an empty segment moves back. A year needs four digits. A date that does not exist cannot be entered: a day past the end of a newly chosen month becomes that month’s last day.
  • Pasting. A whole date or time pasted onto any segment fills the row. It is read as three groups of digits in the locale’s order, and a first group of four digits is read as the year, so a pasted ISO date works everywhere. Copy and cut take the whole row.
  • The value is empty until every segment is filled and the result is available. input is emitted when the value changes — the last segment is filled, a filled row is edited, or it is emptied — and not for a segment that leaves the value empty. change is emitted when focus leaves the field’s segments and picker button, or the action key is pressed, with a changed value. Moving between segments commits nothing.
  • The entry error. A field left half-filled shows invalidText. One left with a value before min, after max or listed in disabledDates shows unavailableText. Either is drawn as an error and emits entryError with its reason (invalid or unavailable); the next edit ends it. It is the one error the field sets itself, and it leaves your error prop alone.
  • What you set is shown as it is, never validated, and emits nothing.

A field’s clock is its locale’s: it has no setting for 12 or 24 hours and no minute step, as the DateTimePicker has.

The picker button. With picker, the field draws an icon-only Button at its end, calendar_month for a date and schedule for a time, named by pickerLabel. Activating it emits pickerRequest with the value, type, min and max; a half-filled row sends an empty value and raises no entry error, because the picker is about to give a whole value. The field presents nothing itself: your product opens a DateTimePicker in the matching mode, anchored to the field when it is docked, and sets the field’s value from the picker’s change. Give the picker the same min, max and disabledDates as the field. Focus returns to the button when the picker closes. The button is disabled with the field and not drawn while it is read-only.

  • Put fixed text that is not part of the value in prefixText and suffixText: a currency sign, a unit.
  • Put a clear or reveal-password action in the trailing slot as your own icon-only Button, with its own name. The picker button is the only action the field builds in, and it comes after yours.
  • Use multiline for text that may run to several lines. By default it grows from minRows to maxRows, then scrolls.
  • Set required on the fields a form asks for, and still say what is missing in errorText when the form is submitted.
  • Use type="code" for a value a machine reads — a pattern, a key, a snippet of configuration — and say what it must be in the label and in supportingText.

Don’t

  • Don’t use the placeholder as the label. It disappears as soon as someone types.
  • Don’t use number for values that are not quantities, such as a card number or a postal code. Use text with the right autocomplete.
  • Don’t mix variants in one form.
  • Don’t put a text field inside a field. It is a field already; a debug build says so. The control a field frames is the text input.
  • Don’t use a text field and your own parsing for a date. Use date, so the locale’s order, the segments’ keys and the entry error come with it.
  • Don’t write “(required)” in the label as well as setting required. The mark already says it, and a screen reader would hear it twice.
  • Don’t let a manually resized field be the only way to see a long text. Give it enough minRows to be useful as it opens.
  • Don’t disable a field only to show a value that cannot change. Use readOnly, so people can still focus, select and copy it.
  • Don’t set a code face on a text field from outside, with ::part(input) or a style of your own, and don’t add dir or spellcheck to the element. Use type="code": the face alone leaves a field that looks right and still corrects what is typed.
  • Don’t use a code field as an editor, and don’t use a disabled one to show long code: a disabled field has no caret to scroll it with. Use readOnly, or a Code block.

Accessibility

Role
textbox
Labeling
The text field adds nothing to what its members expose, and the one thing it coordinates — the drawn count (TXF-67) — is kept from assistive technology, which hears the count from the input: the Field gives the text input its name, its description and its required, invalid and disabled states (FLD-8, FLD-10, FLD-11, FLD-14), and the text input carries them on its own element (TXI-23). Assistive technology meets one text box — or, for a date or a time, one group of spin buttons — named by label, exactly as it meets a Field around a text input written by hand.
Minimum target
The hit area meets the minimum target size even when the visual size is smaller. minimum 32px · pointer 44px · spacing 8px · touch 48px

Keyboard

Keys Action
Every key The text input's (its accessibility block; TXI-3, TXI-41, TXI-43 to TXI-45, TXI-59). The text field handles no key and adds no focus stop (TXF-64).

Notes

  • One level down is the same thing: a Field with a text input in it. Every fixture has that pair as its twin, and the accessibility tree of the two is the same (TXF-62). What the pair by hand does not do by itself is keep the Field's count text equal to the input's count as the person types (TXF-67): the product writes it at each input.
  • On the web the two members stand in the text field's own shadow root, so the form meets the text field and not the input (TXF-63); focus passes through to the input (TXF-64).

Generated from specs/text-field.spec.json, tokens/sys/measure.tokens.json

What the component handles

  • Text is edited in the platform’s own text element, so input methods, dictation, autofill, selection and the clipboard work as they do elsewhere on the platform. A date or a time is the stated exception: its row of segments is the field’s own.
  • A date or time field is a group named by the label. Each segment is a spin button named by its unit and the label (“month, Start date”), with its range and its value; an empty one reads segmentEmptyText.
  • A required field is exposed as required. The mark itself is hidden from assistive technology, so it is not read twice; the optional text is read with the field’s description.
  • The resize handle is a focus stop named by resizeLabel, exposed as a separator whose value is the number of lines. The arrow keys, Home and End resize, so nothing depends on a drag, and its hit area meets the minimum target.
  • The label is the accessible name, whether it is drawn or hidden. The placeholder never is.
  • The shown supporting or error text and the count are the field’s description. So are the prefix and suffix while they are shown, so a unit or currency is read with the value.
  • It exposes the disabled state, and the invalid, required and read-only states where the platform has them. An entry error in a date or time field is exposed exactly as error is.
  • The picker button is a Button named by pickerLabel, exposed as opening a dialog, and a Tab stop after the last segment.
  • When an error appears or changes while the field has focus, it is announced once, politely. The count is announced as it is shown (“12/40”) once typing pauses for a second, never on every keystroke.
  • A search field is exposed as a search box where the platform has that role.
  • A code field is the same text box as every other type, multi-line with multiline: no role, role description or word says that it holds code, and no part of its value is marked as misspelled. Tab always leaves it. Where its lines do not wrap, the caret scrolls it, and focus is shown on the container, which does not scroll.
  • Focus is shown by the field’s own edge, whether it came from the keyboard or a pointer: a line inside a stacked field’s box, and the thicker, colored edge of a filled or outlined field. The hit area meets the minimum target.
  • A press on the label or on the box moves focus to the input and operates nothing. A press on the help, the error text or the count does nothing.
  • The name, the description and the states are tied exactly as a field ties them to any control it frames. The field written by hand, one level down, is the same text box.
  • A missing label is reported in debug builds.

Known platform gaps:

  • SwiftUI, Compose, Flutter and React Native keep the required flag, which draws no mark, and have no resize modes, no handle and no segments.
  • Web: the row of segments shifts by a few pixels when a month or a minute takes its first digit; input can fire twice where the locale’s last segment has two digits; on an Android on-screen keyboard, Backspace on an empty segment cannot move back; and a row wider than a narrow field overflows instead of scrolling.
  • A product cannot empty a half-filled date row by setting value to the empty string it already has.
  • Web: under the glow focus style, a filled field’s glow is rounded at the bottom corners, which are square.
  • Web: input-method composition and the autofilled label are tested in Chromium only.
  • Web: a read-only code field whose lines do not wrap is scrolled from the keyboard as the browser scrolls any text it does not edit — with the caret in Firefox, with the arrow keys in Chrome, with Option and the arrow keys in Safari. Firefox returns such a field to its left edge when the page’s direction changes.
  • SwiftUI, Compose, Flutter and React Native have not built the code type: there type has no code and the field has no wrap.
  • SwiftUI has no invalid, required or read-only states. They are drawn or enforced, not announced. An error is heard only as its text in the description.
  • SwiftUI cannot see input-method composition, so maxLength also applies to text that is still being composed, and a value you set during a composition is not held back.
  • SwiftUI: selected text is highlighted in the caret’s tint, not in the field’s selection color.
  • SwiftUI draws a read-only field’s value as selectable text, not as a text field.
  • SwiftUI reads .labelsHidden() only from iOS 18 and macOS 15. On earlier versions the label is still drawn.
  • React Native cannot see input-method composition. A value you change during a composition, and the maxLength limit, are applied at once and can end the composition.
  • React Native has no invalid, required or read-only states. An error is heard only as its text in the description; required and read-only are not announced.
  • React Native: announcements, autofill and the keyboard for each type are tested with mocks only, not yet on a device.
  • React Native on iOS has one tint for the caret and the selection highlight. The field keeps the caret color, so the highlight is a tint of it rather than the field’s selection color.
  • React Native: a filled field’s glow follows only its top corners, and the placeholder and caret colors switch at once instead of changing over the state motion.
  • Flutter exposes a search field as a search input type, not the search box role.
  • Flutter draws no selection handles or selection toolbar on Android and other touch screens. Long-press and keyboard shortcuts still select and copy; iOS shows its system menu.
  • Flutter: an interactive element in the leading slot comes before the input in focus order, not after it.
  • Compose has no required state: the field reads “Required” as its state description instead.
  • Compose: maxLength and the counter count characters as people see them, but emoji sequences and flags are not tested. The tests run on the JVM under Robolectric, whose character rules do not know them.
  • Compose: TalkBack’s announcements of the error and the count, a real input method and a real autofill service have not been tested on a device.

What you need to do

  • Write a label that names the value, and keep it even when you hide it.
  • Write error text that says how to fix the value, and set error yourself. On SwiftUI and React Native, that text is the only way the error is heard. For a date or time field, also write invalidText and unavailableText, and give segmentEmptyText: without it an empty segment is read with no value.
  • Give the picker button its pickerLabel in your product’s language, and open the picker from pickerRequest.
  • Set required or optional, and give requiredText and optionalText: on the field, or once for the whole product with setWords({ requiredText, optionalText }) — as resizeLabel and segmentEmptyText can be. Tanrei ships no words on the web; a missing one is reported in development. See The product’s words. Until SwiftUI, Compose, Flutter and React Native catch up, their fields draw no mark, and SwiftUI and React Native do not announce the required state: say “required” in the label there.
  • Name the resize handle with resizeLabel. Without it the handle is drawn and works, and has no name.
  • Put a unit or currency in prefixText or suffixText, not in the value, so that it is read with the value and not typed.
  • For a code field, say what the code must be in label and supportingText (“Webhook body”, “JSON”), and say what is wrong with it in errorText: the field does not know the language.
  • Give a trailing icon button its own name in your product’s language.
  • On Compose, the words “Invalid” and “Required” come from the library’s string resources (tanrei_invalid, tanrei_required). Override them in your resources to translate them.
  • On the web, iOS Safari zooms into an input whose text is smaller than 16 px. The default system’s fields use 16 px text at both sizes; a layer that makes it smaller brings the zoom back.