Input is the typed field. Field is the label, helper and error around any control — an
input, a select trigger, a row of chips. Every form is made of the two.
Light
Empty · placeholder in text.tertiary
Filled
Focus · 2px border.focus
Error · helper survives
Per person, in dollars.
Enter a price like 45.00.
Multiline · 88 floor, grows
Dark
Empty · placeholder in text.tertiary
Filled
Focus · 2px border.focus
Error · helper survives
Per person, in dollars.
Enter a price like 45.00.
Multiline · 88 floor, grows
Source:
Input.tsxandField.tsx. The form these sit in: form.
When to use it
Inputfor anything typed: a name, a price, a note.multilinefor long text,multiline="grow"for a note that is usually one line and occasionally two.Fieldaround every control in a form, so the rhythm and the error contract come from one place.
Not for choosing from a list — that is a select trigger. Not for search — that is SearchField, which is this input with two slots filled. Never inside a white card: inputs sit on the page.
Anatomy
| Part | Token |
|---|---|
| Field label | typography.label 13/700, sentence case, text.primary; "Optional" in caption text.secondary on the right |
| Label to control | spacing.s6 |
| Control fill | surface.card |
| Control edge | 1px border.control, radius.input 8 |
| Control padding | spacing.s12 |
| Height floor | controlHeight.control 44; multiline 88 (controlSize.multilineMinHeight) and grows |
| Value | typography.body 15/400, text.primary |
| Placeholder | text.tertiary on surface.card — legal only because that is the control's fill |
| Helper and error | typography.caption, spacing.s6 below the control; helper text.secondary, error feedback.errorText |
| Field to next field | spacing.s16; to the next section spacing.section 24 |
States
- Empty shows the placeholder; filled shows the value.
- Focus: the edge becomes 2px
border.focus, and the padding gives back the extra pixel so nothing moves. Focus wins over the error border — you need to know where you are before you need to know what is wrong. - Error: the edge swaps to
feedback.errorBorder(still 1px) and the message appears below. The helper stays: it explains the field, the error explains the failure. - Validation timing: on blur, never on keystroke.
- In flight: the form sets
editable={false}on every input while it submits. - Disabled: none. A field you cannot change is a fact, shown as text or a static Row.
Content
- The label names the thing ("Event name"), not an instruction ("Enter the event name").
- Mark what is optional. Required is the default and gets no asterisk.
- Placeholders show an example or name the content ("Add a note…"); they never replace the label.
- Errors say what to do in plain words: "Enter a price like 45.00."
- Return moves to the next field. It never submits a form.
Accessibility
- The field's label does not reach a React Native control on its own. Spread
fieldControlProps({ label, value, helper, error })onto the control: it setshasErrorand anaccessibilityLabelread as one sentence — label, value, helper, then "Error: …". - The error text under the field is hidden from screen readers, because it is already in the control's label; announcing it twice is worse than once.
text.tertiaryonsurface.cardis 4.74:1 in light, which is why the placeholder may use it inside the control and never on the page.border.controlonsurface.pageis 3.15:1 in light, so the control's edge is visible against the page it sits on.
Do and don't
The event
Do
Inputs sit on the page, grouped by space and an eyebrow.
Don’t
An input inside a white card is white on white.
Do
Mark what is optional. Everything else is assumed required.
Don’t
An asterisk on every field marks nothing.
Code
import { Field, Input, fieldControlProps } from '../../components/ui';
<Field label="Price" helper="Per person, in dollars." error={errors.price}>
<Input
value={price}
onChangeText={setPrice}
onBlur={validatePrice}
keyboardType="decimal-pad"
placeholder="45.00"
testID="event-price"
{...fieldControlProps({ label: 'Price', value: price, helper: 'Per person, in dollars.', error: errors.price })}
/>
</Field>