Skip to content
TrademarkTrademark
Docs
Form Controls

Input

Updated: September 13, 2026
View on GitHub

The Input component provides a text entry field with floating labels, validation support, and customizable styling. Use it for collecting user input in forms.

Error

The Input component supports error state and message separately. Access them with the error and errorMessage props.

Validation

The Input component supports custom validation through the validate prop. This prop accepts a function that receives the input value and returns either an error message or null. The validation is happens automatically on value change with an automatic debounce of 1 second.

Prefix & Suffix

Add icons or other elements before or after the input text.

Placeholder

Use the placeholder prop to use placeholder instead of label. The placeholder disappears when the input has value.

Description

Use the description prop to add a description below the input.

Character count

Display a character counter with color-coded feedback based on remaining characters. The counter changes color when approaching the limit.

Size

The Input component supports six sizes, from xxs (32px) through xs (40px), s (48px), m (56px, the default) and l (64px) to xl (72px).

Toolbar size

Field heights run a step above the button scale: an xs field is 40px, as tall as an m Button. For a toolbar or a filter row, where every control should be the height of a size="s" Button, use size="xxs". It is 32px, rounded at radius-m like the button, with its prefix and suffix 8px in from the edge.

A 32px field has no room for a floating label. At xxs the label shows as the placeholder, unless you pass a placeholder of your own, and stays the field's accessible name through a visually hidden <label>. Every field built on Input (Select, NumberInput, DateInput, DateRangeInput, PasswordInput, ColorInput, TagInput) and Textarea take xxs the same way, and Form size="xxs" passes it to every field inside.

On touch devices

iOS Safari zooms the page when a focused control computes below 16px, and it does not reliably zoom back out — the page is left scrolled somewhere else. Once UI's mobile base is 15px, which put three of the five sizes under that threshold, size="m" among them.

On touch pointers only, the value is therefore clamped:

clamp
font-sizemax(token, 16px)
line-heightmax(token, 1.3 × font-size)
min-height (xs, s)40px and 48px

xxs has no height floor: it is small on purpose, and a floor would make it the same field as xs on a phone. Its 16px value and 20.8px line still fit its 22px box.

Line-height is clamped alongside the font because xs and s resolve --line-height-body-s to 16.88px on mobile — 16px glyphs would otherwise sit in a line box barely taller than themselves.

Nothing changes on a mouse pointer, and l and xl are untouched either way: they already cleared 16px. The visible consequence is that xs, s and m render the same text size on a phone and differ by height alone, which is how iOS treats its own text fields.

The floating label and the value are positioned from where their ink falls — cap height, baseline and descender — so the pair stays optically centred at whatever size and height the clamps produce.

Variants

The Input component supports two variants: default and ghost.

Loading

Set loading to show a spinner in the input suffix while async validation or data fetching is in progress.

Focus ring

Fields draw no focus ring by default — the quiet, borderless look modern native inputs have settled on. Set focusRing to outline the field on focus, using the same ring Card and SmartLink use, so a keyboard user can see where they are. Worth turning on for any form someone has to tab through.

Note it shows on a mouse click too: :focus-visible always matches a field that takes keyboard input, however that field was focused.

Built on Input

Four components build on Input. They keep everything on this page and add one thing each, so they live on pages of their own:

  • NumberInput — steppers, min/max/step, and a numeric onChange
  • OTPInput — one box per digit, with paste and an onComplete
  • PasswordInput — a reveal toggle
  • TagInput — an array of removable chips instead of a string

API reference

Type
Default
idrequired
Ties the label to the field. Required, and must be unique on the page.
string—
label
Floats above the value once the field is focused or filled. At size xxs it shows as the placeholder instead, and stays the accessible name.
string—
placeholder
Shown while the field is empty. Not a substitute for a label.
string—
size
Field height and type scale: xxs 32px, xs 40px, s 48px, m 56px (default), l 64px, xl 72px. xxs matches a size="s" Button, for toolbars.
xllmsxsxxs
—
error
Put the field in its error state.
booleanfalse
errorMessage
Message shown under the field when error is set.
ReactNode—
description
Helper text under the field, for the rule the label cannot carry.
ReactNode—
corners
Round only some corners — for stacking fields into one block by hand. Inside a [Form](/form-controls/form) with density="stacked" this is worked out for you.
nonetoprightbottomlefttop-lefttop-rightbottom-rightbottom-left
—
className
string—
style
React.CSSProperties—
prefix
Rendered before the value: an icon, a currency symbol, a select.
ReactNode—
suffix
Rendered after the value: a unit, a clear button, a Kbd hint.
ReactNode—
variant
"ghost" drops the border and background, for a field inside a surface that already has them.
defaultghost
default
focusRing
booleanfalse
characterCount
Show how much has been typed, against maxLength.
boolean—
cursor
Set "interactive" for a read-only field that opens something on click.
undefinedinteractive
—
validate
Run on a debounce; return a message to show, or null to clear.
(value: ReactNode) => ReactNode | null—
loading
Show a spinner in the suffix while something resolves.
booleanfalse
span
How many columns to occupy inside a [Form](/form-controls/form) grid. Ignored outside one.
number—
…and every React.InputHTMLAttributes propinherited—
Edit this page on GitHub