XDSTextInput@xds/core · TextInput

Usage

TextInput collects short-form text like names, emails, or search queries. Use it for single-line values where the expected input is brief. Pair it with validation status to guide users through required or formatted fields.

Best practices

GuidancePractices
DoAlways provide a visible label so users know what the field is for. Only hide the label when surrounding context makes it obvious, like a search bar with a magnifying-glass icon.
DoUse validation status with a message to explain what went wrong — "Email must include @" is better than just turning the border red.
DoSize the input to match the expected content length so users can gauge how much to type — small for zip codes, medium for names, large for URLs.
DoAdd a clear button for search and filter inputs so users can quickly reset without selecting all text.
Don'tDon't use placeholder text as a replacement for a label — placeholders disappear on focus and are not reliably read by screen readers.
Don'tDon't use TextInput for multi-line content like comments or descriptions — use TextArea instead.
Don'tDon't mark every field as required — only flag truly mandatory fields so users are not overwhelmed by validation errors.

Anatomy

ElementDescription
LabelrequiredText that identifies the field. Always rendered for accessibility even when visually hidden.
DescriptionHelper text between the label and the input that provides additional context or formatting hints.
Start iconA leading icon inside the input that hints at the expected content, like a magnifying glass for search.
PlaceholderHint text shown when the input is empty. Disappears on focus.
Clear buttonA trailing × button that resets the value and returns focus to the input.
SpinnerLoading indicator that appears during async actions like server-side validation.
Status iconA trailing icon (error, warning, or success) that communicates validation state.

Import

ts
import {XDSTextInput} from '@xds/core/TextInput'

Props

PropTypeDescription
labelrequired
stringLabel text for the input — always rendered for accessibility.
valuerequired
stringCurrent value of the input.
type
'text' | 'password' | 'email' (default: 'text')The HTML input type.
onChange
(value: string, e: ChangeEvent<HTMLInputElement>) => voidCallback fired when the input value changes.
changeAction
(value: string, e: ChangeEvent<HTMLInputElement>) => void | Promise<void>Async action fired after onChange (if not prevented). Triggers optimistic update and shows a loading spinner while pending.
size
'sm' | 'md' | 'lg' (default: 'md')Size variant of the input.
isLabelHidden
boolean (default: false)Visually hides the label while keeping it accessible to screen readers.
description
stringDescription text displayed between the label and input.
isOptional
boolean (default: false)Displays an "Optional" indicator next to the label. Mutually exclusive with isRequired.
isRequired
boolean (default: false)Displays a "Required" indicator next to the label and sets aria-required. Mutually exclusive with isOptional.
isDisabled
boolean (default: false)Disables the input, preventing interaction and dimming the element.
isLoading
boolean (default: false)Puts the input in a loading state, showing a spinner and setting aria-busy.
placeholder
stringPlaceholder text shown when the input is empty.
labelTooltip
stringTooltip text displayed in an info icon at the end of the label.
startIcon
XDSIconTypeSVG icon component displayed at the start of the input. See `npx xds docs icons` for valid semantic names.
status
{type: 'error' | 'warning' | 'success', message?: string}Validation status — applies a colored border and status icon. If message is provided, displays a floating message below the input. Error type also sets aria-invalid.
hasClear
boolean (default: false)Shows a clear (×) button when the input has a value. Clicking it clears the value and returns focus to the input.
hasAutoFocus
boolean (default: false)Automatically focuses the input on mount.
htmlName
stringThe HTML name attribute for the input, useful for form submissions.

Examples

Common configurations, variations, and states.
TextInput — IconInputs with a leading icon that hints at the expected content. Use when the icon helps users identify the field faster, like a lock for passwords or an envelope for email.
tsx
'use client';
import {useState} from 'react';
import {XDSTextInput} from '@xds/core/TextInput';
import {XDSStack} from '@xds/core/Layout';
import {
EnvelopeIcon,
LockClosedIcon,
UserIcon,
} from '@heroicons/react/24/outline';
export default function TextInputIcon() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
return (
<div style={{width: 300}}>
<XDSStack direction="vertical" gap={3}>
<XDSTextInput
label="Full name"
value={name}
onChange={setName}
placeholder="Sarah Chen"
startIcon={UserIcon}
/>
<XDSTextInput
type="email"
label="Email"
value={email}
onChange={setEmail}
placeholder="sarah@company.com"
startIcon={EnvelopeIcon}
/>
<XDSTextInput
type="password"
label="Password"
value={password}
onChange={setPassword}
placeholder="Enter your password"
startIcon={LockClosedIcon}
/>
</XDSStack>
</div>
);
}
TextInput — SearchSearch input with a hidden label, start icon, and clear button. Use for toolbar and header search bars where the icon provides sufficient context.
tsx
'use client';
import {useState} from 'react';
import {XDSTextInput} from '@xds/core/TextInput';
import {XDSStack} from '@xds/core/Layout';
import {MagnifyingGlassIcon} from '@heroicons/react/24/outline';
export default function TextInputSearch() {
const [query, setQuery] = useState('');
const [filter, setFilter] = useState('design systems');
return (
<div style={{width: 300}}>
<XDSStack direction="vertical" gap={3}>
<XDSTextInput
label="Search field"
value={query}
onChange={setQuery}
placeholder="Search projects…"
startIcon={MagnifyingGlassIcon}
hasClear
/>
<XDSTextInput
label="Search field with value"
value={filter}
onChange={setFilter}
placeholder="Filter…"
startIcon={MagnifyingGlassIcon}
hasClear
/>
</XDSStack>
</div>
);
}
TextInput — SizesSmall, medium, and large inputs side by side. Use small in dense UIs like table filters, medium for most forms, and large for prominent single-field pages.
tsx
'use client';
import {useState} from 'react';
import {XDSTextInput} from '@xds/core/TextInput';
import {XDSStack} from '@xds/core/Layout';
export default function TextInputSizes() {
const [sm, setSm] = useState('');
const [md, setMd] = useState('');
const [lg, setLg] = useState('');
return (
<div style={{width: 300}}>
<XDSStack direction="vertical" gap={3}>
<XDSTextInput
label="Small"
value={sm}
onChange={setSm}
placeholder="Enter a value"
size="sm"
/>
<XDSTextInput
label="Medium"
value={md}
onChange={setMd}
placeholder="Enter a value"
size="md"
/>
<XDSTextInput
label="Large"
value={lg}
onChange={setLg}
placeholder="Enter a value"
size="lg"
/>
</XDSStack>
</div>
);
}
TextInput — StatesError, warning, and success validation states with status messages. Use to show users what went wrong and how to fix it.
tsx
'use client';
import {useState} from 'react';
import {XDSTextInput} from '@xds/core/TextInput';
import {XDSStack} from '@xds/core/Layout';
export default function TextInputStates() {
const [error, setError] = useState('sarah@');
const [warning, setWarning] = useState('sarah_chen');
const [success, setSuccess] = useState('https://sarahchen.dev');
const [errorOnly, setErrorOnly] = useState('test');
return (
<div style={{width: 300}}>
<XDSStack direction="vertical" gap={3}>
<XDSTextInput
label="Error message"
value={error}
onChange={setError}
placeholder="Enter a value"
status={{
type: 'error',
message: 'Please enter a valid email address.',
}}
/>
<XDSTextInput
label="Warning message"
value={warning}
onChange={setWarning}
placeholder="Enter a value"
status={{
type: 'warning',
message: 'This username is already taken — try adding a number.',
}}
/>
<XDSTextInput
label="Success message"
value={success}
onChange={setSuccess}
placeholder="Enter a value"
status={{type: 'success', message: 'URL is valid and reachable.'}}
/>
<XDSTextInput
label="Status without message"
value={errorOnly}
onChange={setErrorOnly}
placeholder="Enter a value"
status={{type: 'error'}}
/>
<XDSTextInput
label="Disabled field"
value=""
onChange={() => {}}
placeholder="Enter a value"
isDisabled
/>
<XDSTextInput
label="Loading field"
value="sarahc"
onChange={() => {}}
isLoading
/>
</XDSStack>
</div>
);
}
TextInput — TypesText, password, and email types plus field-level features: tooltip, required, optional, description, disabled, and loading.
tsx
'use client';
import {useState} from 'react';
import {XDSTextInput} from '@xds/core/TextInput';
import {XDSStack} from '@xds/core/Layout';
export default function TextInputTypes() {
const [password, setPassword] = useState('hunter42');
const [email, setEmail] = useState('sarah@example.com');
const [tooltip, setTooltip] = useState('');
const [required, setRequired] = useState('');
const [optional, setOptional] = useState('');
const [described, setDescribed] = useState('');
return (
<div style={{width: 300}}>
<XDSStack direction="vertical" gap={3}>
<XDSTextInput
label="Default field"
value={described}
onChange={setDescribed}
placeholder="Enter your email"
description="Descriptions can be used to provide additional information about a field."
/>
<XDSTextInput
type="password"
label="Password field"
value={password}
onChange={setPassword}
placeholder="Enter a value"
/>
<XDSTextInput
type="email"
label="Email field"
value={email}
onChange={setEmail}
placeholder="Enter a value"
/>
<XDSTextInput
label="Field tooltip"
value={tooltip}
onChange={setTooltip}
placeholder="Enter your API key"
labelTooltip="Your unique API key for authentication. Keep this secret!"
/>
<XDSTextInput
label="Required field"
value={required}
onChange={setRequired}
placeholder="Enter your username"
isRequired
/>
<XDSTextInput
label="Optional field"
value={optional}
onChange={setOptional}
placeholder="Enter your nickname"
isOptional
/>
</XDSStack>
</div>
);
}

Showcase source

tsx
'use client';
import {XDSTextInput} from '@xds/core/TextInput';
export default function TextInputShowcase() {
return (
<div style={{width: 300}}>
<XDSTextInput
label="Name"
value=""
onChange={() => {}}
placeholder="Enter your name"
/>
</div>
);
}