Text Input
A styled, accessible text input component that captures user-entered text data.
import {TextInput} from "@qualcomm-ui/react/text-input"Examples
Simple
The simple <TextInput> bundles all subcomponents together into a single component.
<TextInput
className="w-72"
hint="Optional hint"
label="Label"
placeholder="Placeholder text"
/>Icons
Add icons using props at the root.
<TextInput
className="w-72"
defaultValue="Both icons"
endIcon={Calendar}
label="Both icons"
startIcon={AArrowDown}
/>Composite
Build with the composite API for granular control. This API requires you to provide each subcomponent, but gives you full control over the structure and layout.
<TextInput.Root className="w-72">
<TextInput.Label>Label</TextInput.Label>
<TextInput.InputGroup>
<TextInput.Input placeholder="Placeholder text" />
<TextInput.ErrorIndicator />
</TextInput.InputGroup>
<TextInput.Hint>Optional hint</TextInput.Hint>
<TextInput.ErrorText>Optional error text</TextInput.ErrorText>
</TextInput.Root>Composite Layout
The composite API is useful for alternative layouts and styles.
<TextInput.Root size="sm">
<div className="flex items-center gap-4">
<TextInput.Label className="font-body-sm-bold w-48">
Project Name
</TextInput.Label>
<TextInput.InputGroup>
<TextInput.Input placeholder="QVSCE" />
</TextInput.InputGroup>
</div>
</TextInput.Root>
<TextInput.Root size="sm">
<div className="flex items-center gap-4">
<TextInput.Label className="font-body-sm-bold w-48">
Project Version
</TextInput.Label>
<TextInput.InputGroup>
<TextInput.Input placeholder="v1.2.3" />
</TextInput.InputGroup>
</div>
</TextInput.Root>Clear Trigger
- When using the simple API, the clear button renders automatically. Change this by setting the clearable prop to false.
- When using the composite API, you render the
InputClearButtondeclaratively within theInputGroup. - The clear button only shows when the input field has text.
<TextInput defaultValue="Simple" />
<TextInput.Root defaultValue="Composite">
<TextInput.InputGroup>
<TextInput.Input />
<TextInput.ClearTrigger />
</TextInput.InputGroup>
</TextInput.Root>Sizes
Customize size using the size prop. The default size is md.
<TextInput
className="w-56"
defaultValue="sm"
size="sm"
startIcon={Search}
/>
<TextInput className="w-60" defaultValue="md" startIcon={Search} />
<TextInput
className="w-64"
defaultValue="lg"
size="lg"
startIcon={Search}
/>States
The following shows how the TextInput component appears in each interactive state.
<TextInput disabled label="Disabled" placeholder="Disabled" />
<TextInput label="Read only" placeholder="Read only" readOnly />
<TextInput label="Required" placeholder="Required" required />
<TextInput
errorText="Invalid"
invalid
label="Invalid"
placeholder="Invalid"
/>Controlled State
Set the initial value using the defaultValue prop, or use value and onValueChange to control the value manually. These props follow our controlled state pattern.
import {type ReactElement, useState} from "react"
import {Button} from "@qualcomm-ui/react/button"
import {TextInput} from "@qualcomm-ui/react/text-input"
export default function TextInputControlledStateDemo(): ReactElement {
const [value, setValue] = useState<string>("Controlled value")
return (
<div className="flex items-end gap-4">
<TextInput.Root
className="w-72"
onValueChange={(updatedValue) => {
console.debug("Value changed:", updatedValue)
setValue(updatedValue)
}}
value={value}
>
<TextInput.Label>Label</TextInput.Label>
<TextInput.InputGroup>
<TextInput.Input placeholder="Placeholder text" />
</TextInput.InputGroup>
</TextInput.Root>
<Button emphasis="primary" onClick={() => setValue("")} variant="outline">
Reset
</Button>
</div>
)
}
Error Text and Indicator
Error messages are displayed using two props:
The error text and indicator will only render when invalid is true.
<TextInput
className="w-64"
errorText="You must enter a value"
invalid={!value}
label="Label"
onValueChange={setValue}
placeholder="Enter a value"
required
value={value}
/>Forms
Choose the form library that fits your needs—we've built examples with React Hook Form and Tanstack Form to get you started.
React Hook Form
Use React Hook Form to handle the input state and validation. ArkType works great for schema validation if you need it.
import {type} from "arktype"
import {Controller, type SubmitHandler, useForm} from "react-hook-form"
import {Button} from "@qualcomm-ui/react/button"
import {TextInput} from "@qualcomm-ui/react/text-input"
const addressSchema = type({
city: "string>0",
state: "string==2",
streetAddress: "string>0",
zipCode: "string>=5",
})
type AddressFormData = typeof addressSchema.infer
export default function TextInputReactHookFormDemo() {
const {
control,
formState: {isSubmitting},
handleSubmit,
setError,
} = useForm<AddressFormData>({
defaultValues: {
city: "",
state: "CA",
streetAddress: "",
zipCode: "",
},
})
const handleFormSubmit: SubmitHandler<AddressFormData> = async (data) => {
const validation = addressSchema(data)
if (validation instanceof type.errors) {
validation.forEach((error) => {
const field = error.path?.[0] as keyof AddressFormData
if (field) {
setError(field, {
message: error.message,
})
}
})
return
}
}
return (
<form
className="mx-auto flex w-full max-w-sm flex-col gap-3"
noValidate
onSubmit={(e) => {
void handleSubmit(handleFormSubmit)(e)
}}
>
<Controller
control={control}
name="streetAddress"
render={({field: {onChange, ...fieldProps}, fieldState: {error}}) => (
<TextInput
className="w-full"
errorText={error?.message}
invalid={!!error}
label="Street Address"
onValueChange={onChange}
placeholder="123 Main St"
required
{...fieldProps}
/>
)}
/>
<Controller
control={control}
name="city"
render={({field: {onChange, ...fieldProps}, fieldState: {error}}) => (
<TextInput
className="w-full"
errorText={error?.message}
invalid={!!error}
label="City"
onValueChange={onChange}
placeholder="San Diego"
required
{...fieldProps}
/>
)}
/>
<div className="grid grid-cols-2 gap-4">
<Controller
control={control}
name="state"
render={({field: {onChange, ...fieldProps}, fieldState: {error}}) => (
<TextInput
className="w-full"
errorText={error?.message}
invalid={!!error}
label="State"
onValueChange={onChange}
placeholder="CA"
required
{...fieldProps}
/>
)}
/>
<Controller
control={control}
name="zipCode"
render={({field: {onChange, ...fieldProps}, fieldState: {error}}) => (
<TextInput
className="w-full"
errorText={error?.message}
invalid={!!error}
label="Zip Code"
onValueChange={onChange}
placeholder="10001"
required
{...fieldProps}
/>
)}
/>
</div>
<div className="mt-2 flex w-full justify-end">
<Button
disabled={isSubmitting}
emphasis="primary"
type="submit"
variant="fill"
>
Save Address
</Button>
</div>
</form>
)
}
Tanstack Form
Tanstack Form handles validation with its built-in validators.
import {useForm} from "@tanstack/react-form"
import {Button} from "@qualcomm-ui/react/button"
import {TextInput} from "@qualcomm-ui/react/text-input"
interface AddressFormData {
city: string
state: string
streetAddress: string
zipCode: string
}
export default function TextInputTanstackFormDemo() {
const form = useForm({
defaultValues: {
city: "",
state: "CA",
streetAddress: "",
zipCode: "",
} satisfies AddressFormData,
onSubmit: ({value}) => {
// Handle successful submission
console.log("Form submitted:", value)
},
})
return (
<form
className="mx-auto flex w-full max-w-sm flex-col gap-3"
onSubmit={(e) => {
e.preventDefault()
form.handleSubmit()
}}
>
<form.Field
name="streetAddress"
validators={{
onChange: ({value}) => {
if (!value || value.trim().length === 0) {
return "Street address is required"
}
return undefined
},
}}
>
{(field) => (
<TextInput
className="w-full"
errorText={field.state.meta.errors?.[0]}
invalid={field.state.meta.errors.length > 0}
label="Street Address"
name={field.name}
onBlur={field.handleBlur}
onValueChange={field.handleChange}
placeholder="123 Main St"
value={field.state.value}
/>
)}
</form.Field>
<form.Field
name="city"
validators={{
onChange: ({value}) => {
if (!value || value.trim().length === 0) {
return "City is required"
}
return undefined
},
}}
>
{(field) => (
<TextInput
className="w-full"
errorText={field.state.meta.errors?.[0]}
invalid={field.state.meta.errors.length > 0}
label="City"
name={field.name}
onBlur={field.handleBlur}
onValueChange={field.handleChange}
placeholder="San Diego"
value={field.state.value}
/>
)}
</form.Field>
<div className="grid grid-cols-2 gap-4">
<form.Field
name="state"
validators={{
onChange: ({value}) => {
if (!value || value.length !== 2) {
return "State must be exactly 2 characters"
}
return undefined
},
}}
>
{(field) => (
<TextInput
className="w-full"
errorText={field.state.meta.errors?.[0]}
invalid={field.state.meta.errors.length > 0}
label="State"
name={field.name}
onBlur={field.handleBlur}
onValueChange={field.handleChange}
placeholder="CA"
value={field.state.value}
/>
)}
</form.Field>
<form.Field
name="zipCode"
validators={{
onChange: ({value}) => {
if (!value || value.length < 5) {
return "Zip code must be at least 5 characters"
}
return undefined
},
}}
>
{(field) => (
<TextInput
className="w-full"
errorText={field.state.meta.errors?.[0]}
invalid={field.state.meta.errors.length > 0}
label="Zip Code"
name={field.name}
onBlur={field.handleBlur}
onValueChange={field.handleChange}
placeholder="10001"
value={field.state.value}
/>
)}
</form.Field>
</div>
<div className="mt-2 flex w-full justify-end">
<Button
disabled={form.state.isSubmitting}
emphasis="primary"
type="submit"
variant="fill"
>
Save Address
</Button>
</div>
</form>
)
}
Tanstack form also supports ArkType.
API
<TextInput />
The TextInput extends the TextInput.Root with the following props:
| Prop | Type | Default |
|---|---|---|
When true, renders a clear button that resets the input value on click.
The button only appears when the input has a value. | boolean | true |
{ | ||
Props applied to the error indicator element. | { | |
string | ||
Props applied to the error text element. | { | |
Optional hint describing the element. This element is automatically
associated with the component's input element for accessibility. | ||
Props applied to the label element. | { | |
Props applied to the input group element. | ||
Props applied to the input element. | any | |
Optional label describing the element. Recommended. This element is
automatically associated with the component's input element for
accessibility. | ||
Props applied to the label element. | { | |
HTML placeholder attribute,
passed to the underlying input element. | string |
booleantrue, renders a clear button that resets the input value on click.
The button only appears when the input has a value.{
render?:
| Element
| ((
props: Props,
) => Element)
}
{
icon?:
| LucideIcon
| ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
{
icon?:
| LucideIcon
| ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
any{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
stringComposite API
<TextInput.Root>
| Prop | Type | Default |
|---|---|---|
The initial value of the input when rendered.
Use when you don't need to control the value of the input. | string | |
The document's text/writing direction. | 'ltr' | 'rtl' | 'ltr' |
Whether the input is disabled. When true, prevents user interaction and
applies visual styling to indicate the disabled state. | boolean | |
| LucideIcon | ||
The id of the form that the input belongs to. | string | |
A root node to correctly resolve document in custom environments. i.e.,
Iframes, Electron. | () => | |
The ids of the elements that are associated with the input. These will be
automatically generated if omitted. | { | |
Controls the visual error state of the input. When true, applies semantic error
styling to indicate validation failure. | boolean | |
The name of the input field. Useful for form submission. | string | |
The callback invoked when the field is focused or blurred. | ( | |
The callback invoked when the value changes. | ( | |
Whether the input is read-only. When true, prevents user interaction while
keeping the input focusable and visible. | boolean | |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement | |
Whether the input is required. When true, the input must have a value for form
validation to pass. | boolean | |
The size of the input field and its elements. Governs properties like font size,
item padding, and icon sizes. | | 'sm' | 'md' |
| LucideIcon | ||
The controlled value of the input | string |
string'ltr' | 'rtl'
booleanstring() =>
| Node
| ShadowRoot
| Document
{
errorText: string
hint: string
input: string
label: string
}
booleanstring(
focused: boolean,
) => void
(
value: string,
) => void
boolean| ReactElement
| ((
props: object,
) => ReactElement)
boolean| 'sm'
| 'md'
| 'lg'
| LucideIcon
| ReactNode
string| Attribute / Property | Value |
|---|---|
className | 'qui-input__root' |
data-disabled | |
data-focus | |
data-invalid | |
data-part | 'root' |
data-scope | 'text-input' |
data-size | | 'sm' |
className'qui-input__root'data-disableddata-focusdata-invaliddata-part'root'data-scope'text-input'data-size| 'sm'
| 'md'
| 'lg'
<TextInput.InputGroup>
<div>
element by default.| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__input-group' |
data-disabled | |
data-focus | |
data-invalid | |
data-part | 'input-group' |
data-readonly | |
data-scope | 'text-input' |
data-size | | 'sm' |
className'qui-input__input-group'data-disableddata-focusdata-invaliddata-part'input-group'data-readonlydata-scope'text-input'data-size| 'sm'
| 'md'
| 'lg'
<TextInput.Label>
<label> element by default.| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__label' |
data-disabled | |
data-focus | |
data-invalid | |
data-part | 'label' |
data-scope | 'text-input' |
data-size | | 'sm' |
className'qui-input__label'data-disableddata-focusdata-invaliddata-part'label'data-scope'text-input'data-size| 'sm'
| 'md'
| 'lg'
<TextInput.Hint>
<div> element by default.| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__hint' |
data-disabled | |
data-part | 'hint' |
data-scope | 'text-input' |
className'qui-input__hint'data-disableddata-part'hint'data-scope'text-input'<TextInput.ClearTrigger>
<button> element by default.| Prop | Type |
|---|---|
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__clear-trigger' |
data-disabled | |
data-part | 'clear-trigger' |
data-scope | 'text-input' |
data-size | | 'sm' |
className'qui-input__clear-trigger'data-disableddata-part'clear-trigger'data-scope'text-input'data-size| 'sm'
| 'md'
| 'lg'
<TextInput.Input>
<input> element.| Attribute / Property | Value |
|---|---|
className | 'qui-input__input' |
data-empty | |
data-focus | |
data-invalid | |
data-part | 'input' |
data-readonly | |
data-scope | 'text-input' |
data-size | | 'sm' |
className'qui-input__input'data-emptydata-focusdata-invaliddata-part'input'data-readonlydata-scope'text-input'data-size| 'sm'
| 'md'
| 'lg'
<TextInput.ErrorText>
<div> element by
default.| Prop | Type |
|---|---|
An icon to display next to the error text. | | LucideIcon |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| LucideIcon
| ReactNode
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__error-text' |
data-part | 'error-text' |
data-scope | 'text-input' |
hidden | boolean |
className'qui-input__error-text'data-part'error-text'data-scope'text-input'hiddenboolean<TextInput.ErrorIndicator>
<div> element
by default.| Prop | Type | Default |
|---|---|---|
lucide-react icon or ReactNode. | | LucideIcon | CircleAlert |
Allows you to replace the component's HTML element with a different tag or component. Learn more | | ReactElement |
| LucideIcon
| ReactNode
| ReactElement
| ((
props: object,
) => ReactElement)
| Attribute / Property | Value |
|---|---|
className | 'qui-input__error-indicator' |
data-part | 'error-indicator' |
data-scope | 'text-input' |
data-size | | 'sm' |
hidden | boolean |
className'qui-input__error-indicator'data-part'error-indicator'data-scope'text-input'data-size| 'sm'
| 'md'
| 'lg'
hiddenboolean
<div>element by default.