TextInput ​

A single-line field for text, numbers, email addresses and other short values. For several lines of text, use Textarea, and for a password, use Password.

label
description
type
size
variant
<TextInput
  label="Email"
  placeholder="[email protected]"
  v-model="value"
/>

Examples ​

The #prefix slot puts an icon inside the field, before the text.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { TextInput } from 'frappe-ui'

const query = ref('')
</script>

<template>
  <TextInput
    v-model="query"
    type="search"
    placeholder="Search tasks"
    aria-label="Search tasks"
    class="w-full max-w-xs"
  >
    <template #prefix>
      <span class="lucide-search size-4 text-ink-gray-5" aria-hidden="true" />
    </template>
  </TextInput>
</template>

Price field ​

type="number" with the .number modifier, so the model holds a number. The #suffix slot shows the currency after the value.

USD

Yearly: 588 USD

vue
<script setup lang="ts">
import { ref } from 'vue'
import { TextInput } from 'frappe-ui'

const price = ref<number | ''>(49)
</script>

<template>
  <div class="flex w-full max-w-xs flex-col gap-3">
    <TextInput
      v-model.number="price"
      type="number"
      label="Monthly price"
      placeholder="0"
    >
      <template #suffix>
        <span class="text-sm text-ink-gray-5">USD</span>
      </template>
    </TextInput>
    <p class="text-p-sm text-ink-gray-5">
      Yearly: {{ price === '' ? '—' : `${price * 12} USD` }}
    </p>
  </div>
</template>

Sign-up email ​

label, description, required and error on one field. The error is set from a @blur listener, which lands on the <input>.

We send a confirmation link to this address.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { TextInput } from 'frappe-ui'

const email = ref('')
const error = ref('')

function validate() {
  if (!email.value) error.value = 'Enter your work email.'
  else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value))
    error.value = 'This does not look like an email address.'
  else error.value = ''
}
</script>

<template>
  <TextInput
    v-model="email"
    type="email"
    label="Work email"
    description="We send a confirmation link to this address."
    placeholder="[email protected]"
    name="email"
    required
    :error="error"
    class="w-full max-w-xs"
    @blur="validate"
  />
</template>

Custom label ​

The #label slot replaces the label text and the required marker. It receives { required }. The #description slot replaces the description text.

Lowercase letters and dashes only.

vue
<script setup lang="ts">
import { TextInput } from 'frappe-ui'
</script>

<template>
  <TextInput placeholder="acme" required>
    <template #label="{ required }">
      <span class="font-semibold">Workspace name</span>
      <span v-if="required" class="text-ink-red-5">*</span>
    </template>
    <template #description>
      <span class="italic">Lowercase letters and dashes only.</span>
    </template>
  </TextInput>
</template>

Behavior ​

Label, description and error ​

label renders above the field and description below it. error renders below the field and hides description. It takes a string, an array of strings (one line each), or an Error, the same values as ErrorMessage. An empty string or an empty array means no error. required adds a red asterisk to the label and sets required on the <input>.

A #description slot is not hidden by error. It renders above the error.

Attributes ​

class and style go on the outer element: the wrapper when a label, description or error shows, and the field box otherwise. Every other attribute and listener goes on the <input>, including name, autocomplete, aria-* and @blur. The <input> has autocomplete="off" unless you pass your own autocomplete.

Numbers come back as strings ​

modelValue accepts string | number, and the component always emits a string. That is what the browser gives it: input.value is a string whatever the type is. Add Vue's .number modifier when you want a number back.

vue
<!-- price is a string: "42" -->
<TextInput type="number" v-model="price" />

<!-- price is a number: 42 -->
<TextInput type="number" v-model.number="price" />

An empty field with .number gives '', not 0, so check for it before you do arithmetic.

Dates and times ​

type="date", "time" and "datetime-local" show the browser's own picker. For the library's pickers, use DatePicker and TimePicker.

Debounce ​

debounce delays update:modelValue by that many milliseconds after the last keystroke. Use it when each change starts a request, such as a search.

Accessibility ​

  • The <label> is linked to the <input>, so screen readers announce it as the field's name. Pass aria-label when there is no visible label, as in the search box above.
  • The description and the error are linked with aria-describedby.
  • With error, the <input> gets aria-invalid="true" and aria-errormessage.
  • With required, the <input> gets aria-required="true", and the label includes hidden "(required)" text. The asterisk itself is hidden from screen readers.

API Reference ​

Show types
typescript
import type {
  InputExposed,
  InputSize,
  InputVariant,
} from '../../composables/inputTypes'
import type { InputLabelingProps } from '../../composables/useInputLabeling'
import type { TextInputTypes } from '../types/TextInput'

export interface TextInputProps extends InputLabelingProps {
  /** HTML input type (text, email, number, password, etc.). */
  type?: TextInputTypes

  /** Visual size of the input. */
  size?: InputSize

  /** Style variant of the input. */
  variant?: InputVariant

  /** Placeholder text shown when the input is empty. */
  placeholder?: string

  /** Disables the input when true. */
  disabled?: boolean

  /** Bound value of the input. */
  modelValue?: string | number

  /** Debounce delay (in ms) before emitting value updates. */
  debounce?: number
}

export interface TextInputEmits {
  /** Fired when the input value changes. */
  'update:modelValue': [value: string]
}

/**
 * Shared `defineExpose` shape for the plain-text input family — `TextInput`,
 * `Textarea`, and `Password` all `defineExpose<TextInputExposed<...>>` this
 * exact type, so a template ref works the same way whichever one it points
 * at. Generic over the native element so `Textarea` can type its
 * `inputElement` as `HTMLTextAreaElement` without a separate declaration.
 */
export interface TextInputExposed<
  TElement extends HTMLElement = HTMLInputElement,
> extends InputExposed {
  /** The native element the user types into. `null` before mount. */
  inputElement: TElement | null
}
type
= "text"
TextInputTypes

HTML input type (text, email, number, password, etc.).

size
= "sm"
InputSize

Visual size of the input.

variant
= "subtle"
InputVariant

Style variant of the input.

placeholder
string

Placeholder text shown when the input is empty.

disabled
boolean

Disables the input when true.

modelValue
string | number

Bound value of the input.

debounce
number

Debounce delay (in ms) before emitting value updates.

label
string

Label rendered above (or beside, for binary controls) the input.

description
string

Helper text rendered below the input. Hidden when `error` is set. A `#description` slot is not: it renders beside the error, and is referenced alongside it.

error
ErrorMessageValue

Error message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. Takes a string, an array of strings, or an `Error` whose `messages` are rendered as stacked lines (with `Error.message` as the fallback). This is the same value `ErrorMessage.message` takes. An empty array and an empty string both mean no error.

required
boolean

Marks the field as required. Renders an asterisk next to the label, with `sr-only` text that announces it, and forwards `required` / `aria-required` to the underlying control where the control's role allows it. `data-required` is set either way.

id
string

HTML id of the underlying control. Auto-generated via `useId()` if omitted.

prefix
—

Content rendered before the input (left side)

suffix
—

Content rendered after the input (right side)

label
{ required: boolean; }

Overrides the rendered label content. Receives `{ required }`.

description
—

Overrides the rendered description content.

update:modelValue
[value: string]

Fired when the input value changes.

inputElement
HTMLInputElement | null

The native element the user types into. `null` before mount.

focus
(options?: FocusOptions) => void

Moves focus to the input.