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.

Optional hint
<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.

Optional hint
<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 InputClearButton declaratively within the InputGroup.
  • 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.

Invalid
<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:

  • invalid
  • errorText (or the TextInput.ErrorText component when using the composite API)

The error text and indicator will only render when invalid is true.

You must enter a value
<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:

PropTypeDefault
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
{
render?:
| Element
| ((
props: Props,
) => Element)
}
Props applied to the error indicator element.
{
icon?:
| LucideIcon
| ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
Optional error that describes the element when invalid is true.
string
Props applied to the error text element.
{
icon?:
| LucideIcon
| ReactNode
render?:
| Element
| ((
props: Props,
) => 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.
{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => 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.
{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
HTML placeholder attribute, passed to the underlying input element.
string
Type
boolean
Description
When true, renders a clear button that resets the input value on click. The button only appears when the input has a value.
Type
{
render?:
| Element
| ((
props: Props,
) => Element)
}
Type
{
icon?:
| LucideIcon
| ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
Description
Props applied to the error indicator element.
Type
string
Description
Optional error that describes the element when invalid is true.
Type
{
icon?:
| LucideIcon
| ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
Description
Props applied to the error text element.
Description
Optional hint describing the element. This element is automatically associated with the component's input element for accessibility.
Type
{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
Description
Props applied to the label element.
Description
Props applied to the input group element.
Type
any
Description
Props applied to the input element.
Description
Optional label describing the element. Recommended. This element is automatically associated with the component's input element for accessibility.
Type
{
children?: ReactNode
render?:
| Element
| ((
props: Props,
) => Element)
}
Description
Props applied to the label element.
Type
string
Description
HTML placeholder attribute, passed to the underlying input element.

Composite API

<TextInput.Root>

Groups all parts of the text input. Renders a <div> element by default.
PropTypeDefault
React children prop.
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
lucide icon, positioned at the end of the input field.
| LucideIcon
| ReactNode
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.
    () =>
    | Node
    | ShadowRoot
    | Document
    The ids of the elements that are associated with the input. These will be automatically generated if omitted.
    {
    errorText: string
    hint: string
    input: string
    label: string
    }
    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.
      (
      focused: boolean,
      ) => void
      The callback invoked when the value changes.
        (
        value: string,
        ) => void
        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
        | ((
        props: object,
        ) => 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'
        | 'lg'
        'md'
        
        lucide icon, positioned at the start of the input field.
        | LucideIcon
        | ReactNode
        The controlled value of the input
        string
        Description
        React children prop.
        Type
        string
        Description
        The initial value of the input when rendered. Use when you don't need to control the value of the input.
        Type
        'ltr' | 'rtl'
        Description
        The document's text/writing direction.
        Type
        boolean
        Description
        Whether the input is disabled. When true, prevents user interaction and applies visual styling to indicate the disabled state.
        Type
        | LucideIcon
        | ReactNode
        Description
        lucide icon, positioned at the end of the input field.
        Type
        string
        Description
        The id of the form that the input belongs to.
        Type
        () =>
        | Node
        | ShadowRoot
        | Document
        Description
        A root node to correctly resolve document in custom environments. i.e., Iframes, Electron.
          Type
          {
          errorText: string
          hint: string
          input: string
          label: string
          }
          Description
          The ids of the elements that are associated with the input. These will be automatically generated if omitted.
          Type
          boolean
          Description
          Controls the visual error state of the input. When true, applies semantic error styling to indicate validation failure.
          Type
          string
          Description
          The name of the input field. Useful for form submission.
          Type
          (
          focused: boolean,
          ) => void
          Description
          The callback invoked when the field is focused or blurred.
            Type
            (
            value: string,
            ) => void
            Description
            The callback invoked when the value changes.
              Type
              boolean
              Description
              Whether the input is read-only. When true, prevents user interaction while keeping the input focusable and visible.
              Type
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              Allows you to replace the component's HTML element with a different tag or component. Learn more
              Type
              boolean
              Description
              Whether the input is required. When true, the input must have a value for form validation to pass.
              Type
              | 'sm'
              | 'md'
              | 'lg'
              Description
              The size of the input field and its elements. Governs properties like font size, item padding, and icon sizes.
              Type
              | LucideIcon
              | ReactNode
              Description
              lucide icon, positioned at the start of the input field.
              Type
              string
              Description
              The controlled value of the input

              <TextInput.InputGroup>

              Container that wraps the input element and optional icons. Renders a <div> element by default.
              PropType
              React children prop.
              Allows you to replace the component's HTML element with a different tag or component. Learn more
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              React children prop.
              Type
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              Allows you to replace the component's HTML element with a different tag or component. Learn more

              <TextInput.Label>

              An accessible label that is automatically associated with the input. Renders a <label> element by default.
              PropType
              React children prop.
              Allows you to replace the component's HTML element with a different tag or component. Learn more
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              React children prop.
              Type
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              Allows you to replace the component's HTML element with a different tag or component. Learn more

              <TextInput.Hint>

              Helper text displayed below the input. Renders a <div> element by default.
              PropType
              React children prop.
              Allows you to replace the component's HTML element with a different tag or component. Learn more
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              React children prop.
              Type
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              Allows you to replace the component's HTML element with a different tag or component. Learn more

              <TextInput.ClearTrigger>

              Button that clears the input value. Renders a <button> element by default.
              PropType
              Allows you to replace the component's HTML element with a different tag or component. Learn more
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Type
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              Allows you to replace the component's HTML element with a different tag or component. Learn more

              <TextInput.Input>

              The text input element. Renders an <input> element.

              <TextInput.ErrorText>

              Error message displayed when the input is invalid. Renders a <div> element by default.
              PropType
              An icon to display next to the error text.
              | LucideIcon
              | ReactNode
              Allows you to replace the component's HTML element with a different tag or component. Learn more
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Type
              | LucideIcon
              | ReactNode
              Description
              An icon to display next to the error text.
              Type
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              Allows you to replace the component's HTML element with a different tag or component. Learn more

              <TextInput.ErrorIndicator>

              Visual indicator displayed when the input is invalid. Renders a <div> element by default.
              PropTypeDefault
              lucide-react icon or ReactNode.
              | LucideIcon
              | ReactNode
              CircleAlert
              
              Allows you to replace the component's HTML element with a different tag or component. Learn more
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Type
              | LucideIcon
              | ReactNode
              Description
              lucide-react icon or ReactNode.
              Type
              | ReactElement
              | ((
              props: object,
              ) => ReactElement)
              Description
              Allows you to replace the component's HTML element with a different tag or component. Learn more