TextField
Takes single- or multi-line text input. The label, the description and the error message are one component.
import { TextField } from 'neba';
<TextField label="Email" value={email} onChange={(event) => setEmail(event.target.value)} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. Even solid is not flooded with colour: a field holds user data |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Height and type scale. The same heights as Button, so a row's baseline holds |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. The surface is white, so it reaches the edge, the focus ring and the caret |
| densityshared | 'default' | 'compact' | 'default' | Padding only: never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. A field is a well, not a surface that floats: rarely raised |
| label | ReactNode | — | Label above the control, wired to it by Base UI's Field |
| description | ReactNode | — | Helper text below the control |
| error | ReactNode | — | Error message below the control. Its presence also turns the field invalid |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| multiline | boolean | false | Renders a textarea instead of an input. Every other axis stays identical |
| rows | number | 3 | Visible rows in multiline mode |
| resize | 'none' | 'vertical' | 'horizontal' | 'both' | 'vertical' | Which way the user may drag it. Horizontal breaks a form's column |
| startIcon | ReactNode | — | Content before the control |
| endIcon | ReactNode | — | Content after the control |
| loading | boolean | false | Spinner in place of endIcon. Typing is deliberately still allowed |
| readOnly | boolean | false | Read-only. Selecting and copying still work |
| disabled | boolean | false | Unavailable |
| fullWidth | boolean | false | Stretches to the width of the container |
| shortcuts | NebaShortcuts<HTMLInputElement | HTMLTextAreaElement> | — | Key combinations to act on, spelled the way Shortcut draws them: { 'Mod+Enter': send }. Mod is Command on a Mac and Control everywhere else. Bound to the control, so event.currentTarget is the input or the textarea. It runs before onKeyDown and does not replace it, and nothing is prevented on your behalf |
| onChange | ChangeEventHandler<HTMLInputElement | HTMLTextAreaElement> | — | The native change event. Reach for `onValueChange` when only the value matters |
| classNames | NebaSlots<'label' | 'shell' | 'control' | 'description' | 'error'> | — | Class names for the parts behind the root. The root itself is className, so there is no root key |
Every native <input> attribute passes straight through. color and size are omitted because they collide with the ones above, and onChange is widened so the same handler types against a <textarea> in multiline mode.
Examples
variant
None of the three weights flood the sheet with colour. What a field holds is text the user typed, and the caret, the selection and the placeholder all have to stay legible on top of it. color shows up in the border, the focus ring and the caret instead.
size
The same heights as Button, so a field and a button in one row share a baseline. On a field the height is a floor rather than a fixed row: text set larger than the step — a cron expression, a code, a figure — grows the shell instead of being clipped by it.
multiline · rows · resize
multiline renders a <textarea> and leaves every other axis alone. rows={1} is exactly as tall as the single-line field. resize defaults to the vertical axis only: horizontal resizing breaks a form's column alignment.
startIcon · endIcon · loading
loading puts a spinner in the endIcon slot and marks the field busy, but typing is still allowed: a field is usually loading because of what was just typed into it.
error · invalid · disabled · readOnly
Give error a message and the field also turns invalid, re-pointing the whole field at the danger family. To mark it invalid without a message, pass invalid directly.
value and onChange
Identical to the native <input>.
shortcuts
shortcuts is a map from a key combination to what it does, written the way Shortcut draws it, so the key a form shows a reader and the key it binds are the same string.
<TextField
label="Message"
multiline
shortcuts={{
'Mod+Enter': (event) => {
event.preventDefault();
send();
},
Escape: clear
}}
/>Mod is Command on a Mac and Control everywhere else. The modifiers are matched exactly, so Enter and Mod+Enter are two entries that never both fire.
It is bound to the control, so event.currentTarget is the <input> or the <textarea> and event.currentTarget.value is what was typed. Nothing is prevented for you: a Mod+Enter that must not also insert a newline calls preventDefault itself. onKeyDown still sees every keystroke and runs after the map: neither prop replaces the other.
classNames
className lands on the root (the column holding the label, the shell and the two lines under it), so the <input> is reached through classNames instead. There is no root key; that is what className already is.
<TextField
label="Email"
className="w-80"
classNames={{ label: 'uppercase tracking-wide', control: 'font-mono' }}
/>The slots are label, shell, control, description and error. shell is the framed box wearing the border, the fill and the focus ring; control is the <input> or <textarea> inside it. See prop conventions for how a class name you pass resolves against the component's own.
Accessibility
label,descriptionanderrorare connected to the control withidandaria-describedby.- There is no floating-label variant.
- The focus ring belongs to the shell rather than the
<input>inside it, so it traces the border. - Clicking the shell's own padding puts the caret in the field.