Input and Textarea

The two text controls — a line and a block — where the border is doing real work rather than decorating.

Try it

Type, then Tab away: the border arrives with focus and leaves with it.

Choosing a control

Input holds one line: a name, an e-mail address, a reference. Textarea holds a block the reader will write more than a sentence into, and it can be resized. When the value comes from a short list the reader should not have to spell, it is a Select; when it is a date, it is the Date picker, whose field is this Input with a mask.

A placeholder shows the expected format — name@example.com, DD/MM/YYYY — and vanishes on the first keystroke, so it is never the label. Every field keeps a visible label above it; the one in Try it is the exception a search box is allowed, and it carries an aria-label instead.

Give the browser what it needs to help. type and inputMode summon the right keyboard on a phone — email puts the @ on it, tel and numeric the digits — and autoComplete with a real name lets a saved address fill itself in. Nothing here blocks paste: people paste passwords and one-time codes, and a field that refuses them is a field they retype wrong.

Anatomy and sizes

A field is four things, top to bottom: the label, the help text, the control, and the error when there is one. Field lays them out and wires them; the control alone is the box.

Three heights, shared with Button, so a button and a field of the same size sit level on one line — the drawn numbering did not give that: a medium field was 48 and a medium button 40. The drawn heights are md and lg; sm is the compact addition. The padding is tighter at the end, 12px against 16px, so an icon sits closer to the edge than the text does. The radius is 12px, not the capsule: that shape belongs to buttons, and a field that borrowed it would blur what you press and what you type into.

Input sizes
SizeHeightBlock paddingInline padding
sm32px4px16 / 12px
md40px8px16 / 12px
lg48px12px16 / 12px

Rest, focus, error

The field is filled and carries no border at rest. A border arrives on focus, and on nothing else — the box does not react to the pointer. A caret already says where typing will land, and a field that lights up under the mouse competes for attention with the one that is actually focused. The focus border is the strongest ink the theme has, and the error border is the danger one; both are measured against the fill in the margin.

The resting state has a known gap, kept on purpose.

With no border, the field’s boundary is carried by its fill alone: 1.07Fail against a card in light, and against the canvas, which is a step away, 1.00Fail. WCAG 1.4.11 asks for 3:1 where a border is what identifies a control. The drawn component has no resting border, and this matches it; the label above and the placeholder inside are what say “field” until focus does. The decision of 2026-09-07 kept a hairline in border/default instead, and the two versions are still to be reconciled.

States

Enter an address that includes an @.

Tab into the read-only field: it shows focus without pretending to be editable.

Read-only keeps a recessed fill and, on focus, a quieter border than an editable field: the text can still be selected and copied, so the field still takes focus and still shows it. Disabled drops the paint and the focus both; it is the one state a keyboard cannot reach.

Field

Written by hand, a labelled field with help text and an error is four things to get right, and three of them fail silently: a mismatched id leaves the label unattached, a missing aria-describedby leaves the error unannounced, and a forgotten invalid flag leaves the field red to sighted users and fine to everyone else.

Field wires all of it. Type something without an @ below — the border turns, the message appears, and the control is marked invalid and described by the message at the same moment.

We only use it for appointment reminders.
Enter an address that includes an @.

The error is an instruction, beside the field that failed: it says what to enter, not what was wrong, and it does not apologise. The help text stays announced alongside it rather than being replaced by it — it usually still applies when the value is wrong, and often explains why.

Field is for text controls. Checkbox and Radio carry their own labels, which belong beside the control rather than above it.

Textarea

The same box, the same fill and the same focus border as Input, from the same stylesheet. It differs where a block of text differs from a line.

One size, and a fixed starting height of 144px rather than a row count — a row count sizes the box from whichever font happens to load, so the field would be a different height before and after the webfont arrives.

Its horizontal padding is symmetric, where a single-line field is tighter on the right to sit closer to its icon. A block of text wants the same margin on both sides.

Visible to the clinician only.

Accessibility

invalid sets aria-invalid. When the field is valid the attribute is left off entirely rather than set to false, which would be noise for a screen reader.

Neither component renders its own label — a control that invents a label is a control that gets the wrong one. Wrap it in a Field, or wire htmlFor yourself. A control still works with neither, so nothing forces the wrapper on you.

A read-only field still takes focus — that is how its text gets selected and copied — so it still shows one. It keeps its recessed fill and takes a quieter border than an editable field, rather than lighting up as though you could type in it. Removing the indicator would leave a focusable element with nowhere visible for the keyboard to be.

Validate on submit, not on every keystroke, and keep the submit button enabled until the request starts: a button that stays disabled until the form is valid tells the reader nothing about which field is holding it up. When a submit fails, mark the failing fields, point each at its message, and move focus to the first one.

Input’s visual size prop shadows the HTML size attribute, which sets a width in characters. Passing size="lg" gives you a taller control, not a forty-character-wide one.

Props

Input props
PropTypeDefault
size'sm' | 'md' | 'lg''md'
invalidbooleanfrom Field, else false
iconStartReactNode
iconEndReactNode
Textarea props
PropTypeDefault
invalidbooleanfrom Field, else false
Field props
PropTypeDefault
labelReactNoderequired
descriptionReactNode
errorReactNode
requiredbooleanfalse

All remaining input and textarea attributes are passed through; inside a Field, the id, the description and the invalid flag come from it unless you pass your own.