Components/Form
TextInput
Text inputs collect a single line of typed text. They suit an answer that is short and free-form, such as a name, an email address or a web address, where no set of options would cover what the reader might need to say.
import { TextInput } from '@growth-protocol-ai/alpha';When to use
Reach for a text input when the answer is a short piece of free-form text the reader types, and give it a visible label — the field is anonymous without one the moment they start typing. Let the field itself say whether it holds a value; that is what a reader reads back when they check their work.
When not to use
Do not use a text input where the answer is one of a known set, because a dropdown or a radio group asks the same question without inviting a typo. An answer that runs past one line belongs in a text area, and a question the page is expected to answer belongs in a search field.
size2 values
Size is geometry only — it never changes what the field means. Both heights are the button’s and the dropdown’s, so a form that mixes the three lines up row by row.
| size | When to use |
|---|---|
regular | 36px. Forms, filter bars and toolbars. |
large | 52px. Hero forms and touch-first surfaces. |
| State | When to use |
|---|---|
Empty | The default frame: --border-default around a placeholder in --text-disabled, and no clear button. Focus lights the same brand border that content does, so on a focused field the frame stops being the signal and the × is what says there is anything to clear. |
Filled | The clear × arrives with the first character and leaves with the last, because filled is read off the value rather than set. The --button-primary border and the inset shadow are shared with focus, which paints them at the same specificity and later in the sheet — so what filled adds is that they survive the blur: a field that holds something says so. |
Display (read-only) | The default frame around real content, with no × and no brand border. Not a disabled control — nothing about it is off, it simply is not taking input — so it stays focusable, selectable and submitted. Confirmation screens, profile cards, summary views. |
Disabled, empty | A --surface-depth-2 fill and no border at all: blank space rather than an editable field. Skipped by the keyboard and left out of the submitted form. |
Disabled, filled | The same fill, but a --text-disabled border keeps the shape of a field, so locked data still reads as data. The clear × is gone in both disabled renderings. |
Give every field a name. A placeholder is not one — it leaves the moment someone types. Pass an id and point a <label htmlFor> at it, or set aria-label.
The clear button has no visible text, so clearLabel is its accessible name. Name the field in it when a form has several: "Clear email", not "Clear".
Clearing returns focus to the field it just emptied, because the button leaves with the content that justified it.
readOnly for data the reader may read and copy, disabled for a control that is switched off. A disabled field is skipped by the keyboard and left out of the submitted form; a read-only one is neither.
className and style dress the frame; every other prop is the input’s. id, name, autoComplete, required and aria-describedby all land where a label, a form library and a screen reader expect to find them.
The default width is 240px — the dropdown trigger’s — so a field and a selector line up in a filter bar. Override it through className when the form wants a column, and then give every field in that form the same one.
The brand border says the field holds something. It does not say the field is right — validation is not in this iteration, and an error state should arrive as a documented variant rather than as a red border passed through className.
Controlled value
value with onChange, where the state lives in yours. The clear × reports through onChange exactly as a keystroke does, so a field that ignores the event will not clear — which is the one way a controlled field can look broken.
| Prop | Type | Default | Notes |
|---|---|---|---|
size | "regular" | "large" | "regular" | 36px or 52px. Geometry only — never changes intent. |
type | "text" | "email" | "password" | "search" | "tel" | "url" | "text" | The single-line kinds this frame draws. number, date and file bring controls of their own and are not here yet. |
value | string | number | — | Controlled value. Pair it with onChange. |
defaultValue | string | number | — | Initial value when uncontrolled. Filled is derived from it, so a field that starts with content starts with the brand border and the ×. |
onChange | (event: ChangeEvent<HTMLInputElement>) => void | — | Every keystroke, and once more with an empty value on clear. |
placeholder | string | — | Shown while the field is empty, in --text-disabled. A hint, not a name. |
disabled | boolean | false | Non-interactive, and repainted rather than faded. |
readOnly | boolean | false | The Display state. Content, no editing, no ×. |
clearable | boolean | true | The clear button, shown once the field holds something. Turn it off where clearing is not an action worth offering. |
clearLabel | string | "Clear" | Accessible name for the ×, which carries no visible text. |
children | never | — | A void <input> takes none. The label goes beside the field, not inside it. |