TextInput
A single-line text field with its label, description, and error built in — Input plus Field, the way every other control in the form family already works.
TextInput is the single-line text field you reach for by default. It's Input — the same shell, the same slots, the same states — carrying its own label, description, error, and required through Field, exactly like Textarea, Select, and the rest of the family.
import { TextInput } from '@42-central/ui-react/text-input';
<TextInput label="Login" placeholder="login@student.42.fr" />Native <input> props (value, onChange, name, type, autoComplete, maxLength, ref, …) forward straight through, so react-hook-form's register() binds it with no wrapper. className styles the shell root and classNames overrides individual parts.
Use TextInput whenever the field has a label — which is nearly always. Reach
for Input when you're composing the shell yourself: inside a
Field you already control, or as the search box of a larger widget. Omit
every field prop and TextInput mounts no Field at all, so the two render
identically.
Playground
Component
Dir
Presets
Props
Code
<Component
label="Login"
size="md"
placeholder="login@student.42.fr"
/>Full prop browser
Label, description & error
label names the control, description is helper text above it, and error renders below it. Passing error flips the field to the invalid (red) state on its own — no separate invalid needed. required adds the semantic flag and a red asterisk.
Code
<TextInput label="Login" placeholder="norminette" />
<TextInput label="Login" description="Your intra handle, not your email." placeholder="norminette" />
<TextInput label="Login" required placeholder="norminette" />
<TextInput label="Login" error="That login is already taken" defaultValue="norminette" />description and error are independent — both can show at once, and the control references each through aria-describedby / aria-errormessage.
Sizes
Five steps — xs through xl. The label, helper text, control font, and slot icons all scale together, so a TextInput lines up with an Input, Textarea, or Select at the same size.
Code
<TextInput size="xs" label="xs" placeholder="xs" />
<TextInput size="sm" label="sm" placeholder="sm" />
<TextInput size="md" label="md" placeholder="md" />
<TextInput size="lg" label="lg" placeholder="lg" />
<TextInput size="xl" label="xl" placeholder="xl" />Variants
Two shells, shared with the rest of the family. default is bordered; unstyled strips the chrome but keeps the layout.
Code
<TextInput variant="default" label="Default" placeholder="Default" />
<TextInput variant="unstyled" label="Unstyled" placeholder="Unstyled" />Slots
startSlot and endSlot are RTL-aware (start = left in LTR, right in RTL), and icons in them auto-size to the field's size. Both wrappers are aria-hidden, so keep them decorative — an interactive control belongs in an InputBase you compose yourself.
Code
<TextInput label="Email" startSlot={<Mail />} placeholder="login@student.42.fr" />Search fields
A search box needs an accessible name, but rarely a visible label — and a placeholder is not a substitute, since it disappears the moment someone types. Pass a real label and hide it with classNames={{ label: 'sr-only' }}: screen readers still announce it, and nothing is added to the layout.
Code
<TextInput
label="Search participants"
classNames={{ label: 'sr-only' }}
startSlot={<Search />}
placeholder="Search participants…"
/>States
invalid swaps the border and focus ring to red (and sets aria-invalid) without rendering a message — use it when the error text lives elsewhere, e.g. driven from a form library's fieldState.invalid. disabled dims the shell and blocks input; readOnly keeps full contrast and stays focusable but rejects edits.
Code
<TextInput label="Login" invalid defaultValue="not-a-login" />
<TextInput label="Login" disabled defaultValue="Locked value" />
<TextInput label="Login" readOnly defaultValue="norminette" />Forms
TextInput is event-based, like Input and Textarea: onChange receives the DOM event, so react-hook-form's register() binds it directly and no Controller is needed. See the Forms guide for the full contract.
<TextInput
label="Email"
type="email"
error={errors.email?.message}
{...register('email', { required: 'Email is required' })}
/>ref lands on the underlying <input>, so react-hook-form's focus-on-error works out of the box. invalid is independent of error, so a form library can flip only the red state when the message renders somewhere else.
Composing with Field
Omit every field prop and TextInput mounts no Field — it renders exactly what a bare Input renders, and still inherits an outer Field you compose yourself. This matters for layouts where one Field wraps several controls:
import { Field } from '@42-central/ui-react/field';
import { TextInput } from '@42-central/ui-react/text-input';
<Field label="Date range" error={errors.range?.message}>
<div className="flex gap-2">
<TextInput type="date" {...register('from')} />
<TextInput type="date" {...register('to')} />
</div>
</Field>API
Prop
Type
TextInput forwards every other prop to the underlying <input> (value, defaultValue, onChange, name, type, autoComplete, maxLength, pattern, …). className styles the shell root.
Accessibility
labelis wired to the input byField— clicking it focuses the control, and screen readers announce it as the control's name.descriptionanderrorare associated througharia-describedbyandaria-errormessage, so both are announced without being read as part of the name.errorsetsaria-invalidon the input, so the error state is announced as well as shown.- Never use
placeholderas the label: it vanishes on first keystroke and is not a reliable accessible name. For a field with no visible label, keeplabeland hide it withclassNames={{ label: 'sr-only' }}— see Search fields. startSlot/endSlotarearia-hiddendecorations; anything interactive must be composed into anInputBaseinstead.- RTL: the shell is a plain flex row, so the slots and padding flip automatically under
dir="rtl".
Changelog
Field
Label, description, and error scaffold for any form control — built on Ark UI's Field, so the accessible name, description, and invalid state wire themselves to whatever control you nest inside.
Input
The styled input shell the whole form family renders into — bordered box, focus ring, slots, and invalid / disabled states. Label, description, and error live in TextInput or Field.