TextInput
A flexible input for entering text, numbers etc. Supports many sizes, styles, and custom slots.
Playground
<TextInput
label="Email"
placeholder="[email protected]"
v-model="value"
/><script setup lang="ts">
import { ref } from 'vue'
import { TextInput } from 'frappe-ui'
const value = ref('')
</script>
<template>
<TextInput v-model="value" placeholder="Enter your name" />
</template>Variants
<script setup lang="ts">
import { TextInput } from 'frappe-ui'
</script>
<template>
<div class="flex flex-col gap-3 w-full max-w-sm">
<TextInput variant="subtle" placeholder="Subtle" />
<TextInput variant="outline" placeholder="Outline" />
<TextInput variant="ghost" placeholder="Ghost" />
</div>
</template>Sizes
<script setup lang="ts">
import { TextInput } from 'frappe-ui'
</script>
<template>
<div class="flex flex-col gap-3 w-full max-w-sm">
<TextInput size="sm" placeholder="Small" />
<TextInput size="md" placeholder="Medium" />
<TextInput size="lg" placeholder="Large" />
<TextInput size="xl" placeholder="Extra large" />
</div>
</template>Types
<script setup lang="ts">
import { TextInput } from 'frappe-ui'
</script>
<template>
<div class="flex flex-col gap-3 w-full max-w-sm">
<TextInput type="text" placeholder="text" />
<TextInput type="number" placeholder="number" />
<TextInput type="email" placeholder="email" />
<TextInput type="date" placeholder="date" />
<TextInput type="datetime-local" placeholder="datetime-local" />
<TextInput type="search" placeholder="search" />
<TextInput type="tel" placeholder="tel" />
<TextInput type="time" placeholder="time" />
<TextInput type="url" placeholder="url" />
</div>
</template>Prefix and suffix slots
<script setup lang="ts">
import { Avatar, TextInput } from 'frappe-ui'
</script>
<template>
<div class="flex flex-col gap-3 w-full max-w-sm">
<TextInput placeholder="Search...">
<template #prefix>
<span class="lucide-search size-4 text-ink-gray-6" />
</template>
</TextInput>
<TextInput placeholder="Amount">
<template #suffix>
<span class="text-p-sm text-ink-gray-6">USD</span>
</template>
</TextInput>
<TextInput placeholder="Display name">
<template #prefix>
<Avatar
size="sm"
image="https://avatars.githubusercontent.com/u/499550?s=60&v=4"
/>
</template>
</TextInput>
</div>
</template>Labeling
label, description, error, and required are wired into the underlying input via the shared labeling contract. Setting error suppresses the description and applies aria-invalid="true".
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Checkbox, TextInput } from 'frappe-ui'
const email = ref('')
const required = ref(true)
const showError = ref(false)
const error = computed(() => (showError.value ? 'Email is required.' : ''))
</script>
<template>
<div class="flex gap-8 items-start">
<TextInput
v-model="email"
label="Email"
description="We'll only use this to send product updates."
:error="error"
:required="required"
placeholder="[email protected]"
class="w-72"
/>
<div
class="flex flex-col gap-2 items-start border-l border-outline-gray-2 pl-6"
>
<Checkbox v-model="required" label="required" />
<Checkbox v-model="showError" label="show error" />
</div>
</div>
</template>Custom label and description slots
The #label slot receives { required } so callers can render their own required indicator.
<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>States
<script setup lang="ts">
import { TextInput } from 'frappe-ui'
</script>
<template>
<div class="flex flex-col gap-4 w-full max-w-sm">
<TextInput label="Default" placeholder="Enter input" />
<TextInput label="Required" required placeholder="Enter input" />
<TextInput label="Disabled" disabled placeholder="Enter input" />
<TextInput
label="With error"
error="This field is required."
placeholder="Enter input"
/>
</div>
</template>API Reference
Show types
import type { 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,
> {
/** Moves focus to the native element. */
focus: (options?: FocusOptions) => void
/** The native element the user types into. `null` before mount. */
inputElement: TElement | null
}HTML input type (text, email, number, password, etc.).
Visual size of the input.
Style variant of the input.
Placeholder text shown when the input is empty.
Disables the input when true.
Bound value of the input.
Debounce delay (in ms) before emitting value updates.
Label rendered above (or beside, for binary controls) the input.
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 message rendered below the input. When set, the control receives `aria-invalid="true"` and `data-state="invalid"`. May be either a string or an `Error` object whose `messages?: string[]` is rendered as stacked lines (with `Error.message` as the fallback).
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.
HTML id of the underlying control. Auto-generated via `useId()` if omitted.
| Slot | Payload |
|---|---|
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. |
Content rendered before the input (left side)
Content rendered after the input (right side)
Overrides the rendered label content. Receives `{ required }`.
Overrides the rendered description content.
| Event | Payload |
|---|---|
update:modelValue | [value: string] Fired when the input value changes. |
Fired when the input value changes.