Skip to content

Text Fields

Text fields are used to allow users to input plain text into a form.

Text fields are implemented with the input element for a single line of text or the textarea element for multiple lines of text.

Features

  • Uses input or textarea elements as a base.
  • Labels, descriptions, and error message displays are automatically linked to input and label elements with aria-* attributes.
  • Validation support with native HTML constraint validation or Standard Schema validation.
  • Support for v-model binding.

Anatomy

Label
Label
Value
Input
Help text
Description or Error Message

Building a Text Field Component

You can start by importing the useTextField composable and using it in your text field component.

The useTextField composable returns binding objects for the elements shown in the anatomy. You will use v-bind to bind them to the corresponding DOM elements.

<script setup lang="ts">
import TextField from './TextField.vue';
import { ref } from 'vue';
const name = ref('');
</script>
<template>
<TextField v-model="name" label="Your name" />
</template>
<script setup lang="ts">
import { type TextFieldProps, useTextField } from '@formwerk/core';
const props = defineProps<TextFieldProps>();
const {
inputProps,
labelProps,
errorMessage,
errorMessageProps,
descriptionProps,
} = useTextField(props);
</script>
<template>
<div class="field">
<label v-bind="labelProps">{{ label }}</label>
<input v-bind="inputProps" />
<div v-if="errorMessage" v-bind="errorMessageProps" class="error">
{{ errorMessage }}
</div>
<div v-if="description" v-bind="descriptionProps" class="hint">
{{ description }}
</div>
</div>
</template>
72 collapsed lines
<style scoped>
.field {
--color-primary: #10b981;
--color-text-primary: #333;
--color-text-secondary: #666;
--color-border: #d4d4d8;
--color-focus: var(--color-primary);
--color-error: #f00;
label {
display: block;
margin-bottom: 0.25rem;
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
}
.hint {
font-size: 13px;
color: var(--color-text-secondary);
opacity: 0;
transition: opacity 0.3s ease;
}
.hint,
.error {
margin-top: 0.25rem;
}
input {
display: block;
width: max-content;
padding: 0.5rem 0.6rem;
font-size: 13px;
color: var(--color-text-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
transition: border-color 0.3s ease;
&:focus {
outline: none;
border-color: var(--color-focus);
box-shadow: 0 0 0 1px var(--color-focus);
}
}
.error {
display: none;
font-size: 13px;
color: var(--color-error);
}
&:has(:focus) {
.hint {
opacity: 1;
}
}
&:has(:user-invalid) {
--color-border: var(--color-error);
--color-focus: var(--color-error);
.error {
display: block;
}
.hint {
display: none;
}
}
}
</style>

Notice that we imported the TextFieldProps in the previous example. This is recommended to use as your component prop types.

Not only do you get type safety for your component out of it, but it also handles the reactivity aspects of the props so you don’t have to. You are free to extend it with your own props or omit the ones you don’t need.

Building a Text Area Component

Instead of using an input[type="text"] element, you can switch to using a textarea element as a base element instead.

<script setup lang="ts">
import TextAreaField from './TextAreaField.vue';
</script>
<template>
<TextAreaField label="Write a poem" />
</template>
<script setup lang="ts">
import { type TextFieldProps, useTextField } from '@formwerk/core';
const props = defineProps<TextFieldProps>();
const {
inputProps,
labelProps,
errorMessage,
errorMessageProps,
descriptionProps,
} = useTextField(props);
</script>
<template>
<div class="field">
<label v-bind="labelProps">{{ label }}</label>
<textarea v-bind="inputProps"></textarea>
<div v-if="errorMessage" v-bind="errorMessageProps" class="error">
{{ errorMessage }}
</div>
<div v-if="description" v-bind="descriptionProps" class="hint">
{{ description }}
</div>
</div>
</template>
73 collapsed lines
<style scoped>
.field {
--color-primary: #10b981;
--color-text-primary: #333;
--color-text-secondary: #666;
--color-border: #d4d4d8;
--color-focus: var(--color-primary);
--color-error: #f00;
label {
display: block;
margin-bottom: 0.25rem;
font-size: 14px;
font-weight: 500;
color: var(--color-text-primary);
}
.hint {
font-size: 13px;
color: var(--color-text-secondary);
opacity: 0;
transition: opacity 0.3s ease;
}
.hint,
.error {
margin-top: 0.25rem;
}
textarea {
display: block;
width: max-content;
padding: 0.5rem 0.6rem;
font-size: 13px;
color: var(--color-text-primary);
border: 1px solid var(--color-border);
border-radius: 6px;
resize: none;
transition: border-color 0.3s ease;
&:focus {
outline: none;
border-color: var(--color-focus);
box-shadow: 0 0 0 1px var(--color-focus);
}
}
.error {
display: none;
font-size: 13px;
color: var(--color-error);
}
&:has(:focus) {
.hint {
opacity: 1;
}
}
&:has(:user-invalid) {
--color-border: var(--color-error);
--color-focus: var(--color-error);
.error {
display: block;
}
.hint {
display: none;
}
}
}
</style>

Validation

HTML Constraints

You can use the following properties to validate the text field with native HTML constraint validation:

NameTypeDescription
maxLengthnumberThe maximum length of characters.
minLengthnumberThe minimum length of characters.
requiredbooleanWhether the text field is required.
patternstring | RegExpA regular expression for validation. Not supported for textarea fields.

In addition to the above properties, if you are using the input element, you can use the built-in validation for the type attribute.

TypeDescription
emailValidates the value as an email.
urlValidates the value as a URL.

Here is an example of how to use the maxLength and minLength properties to limit the text length between 3 and 18 characters.

Assuming you have a TextField component like the one shown above, you can use it like this:

<script setup lang="ts">
import TextField from './TextField.vue';
</script>
<template>
<TextField
label="HTML Constraints Demo"
maxLength="8"
minLength="3"
required
/>
</template>

Standard Schema

useTextField also supports Standard Schema validation through the schema prop. This includes multiple providers like Zod, Valibot, Arktype, and more.

<script setup lang="ts">
import { z } from 'zod';
import TextField from './TextField.vue';
const schema = z.string().min(3).max(8);
</script>
<template>
<TextField label="Standard Schema" :schema="schema" />
</template>

Mixed Validation

All text fields created with Formwerk support mixed validation, which means you can use both HTML constraints and Standard Schema validation to validate the field, and they work seamlessly together.

Note that HTML constraints are validated first, so any errors from the HTML constraints will be displayed first. Then, once all HTML constraints are satisfied, the Standard Schema is validated.

<script setup lang="ts">
import { z } from 'zod';
import TextField from './TextField.vue';
const schema = z.string().min(3).max(20);
</script>
<template>
<TextField label="Mixed Validation" :schema="schema" type="url" required />
</template>

This makes schemas lighter; however, we recommend sticking to one or the other per form for maintainability.

If you need to disable the native validation, you can do so by setting the disableHtmlValidation prop to true.

<script setup lang="ts">
import { z } from 'zod';
import TextField from './TextField.vue';
const schema = z.string().min(3).max(20);
</script>
<template>
<TextField
label="HTML Validation Disabled"
disable-html-validation
:schema="schema"
type="url"
required
/>
</template>

You can also disable it globally for all fields. For more information, check out the Validation guide.

Usage

Disabled

Use disabled to mark fields as non-interactive. Disabled fields are not validated and are not submitted.

If you need to prevent the user from interacting with the field while still allowing it to submit, consider using readonly instead.

<script setup lang="ts">
import TextField from './TextField.vue';
</script>
<template>
<TextField label="Disabled Field" disabled />
</template>

Readonly

Readonly fields are validated and submitted, but they do not accept user input. The field is still focusable, and the value is copyable. For more info, check the MDN.

<script setup lang="ts">
import TextField from './TextField.vue';
</script>
<template>
<TextField label="Readonly Field" value="You can't change me" readonly />
</template>

RTL

The text field doesn’t require much for RTL support; however, the dir prop can be used to set the direction of the field for convenience.

<script setup lang="ts">
import TextField from './TextField.vue';
</script>
<template>
<TextField name="fullName" label="ما هو اسمك؟" dir="rtl" />
</template>

Styling

Formwerk does not come with any markup or styling, which is often the part that makes a design system unique. That means you can use any styling solution you want, whether it’s TailwindCSS or plain CSS, as you have already seen.

Note that we make use of the :user-invalid pseudo-classes to style the field and control when to show the error messages without any JavaScript. Formwerk leans into native APIs and browser features to provide a more seamless experience for the user. In fact, even if you use Standard Schemas to validate the field, the pseudo-classes will still work.

For more information on styling and recommendations, check the Styling guide.

API

Props

These are the properties that can be passed to the useTextField composable.

NameTypeDescription
descriptionDescription text that provides additional context about the field.
disabledWhether the field is disabled.
disableHtmlValidationWhether to disable HTML5 validation.
labelThe label of the text field.
maxLengthMaximum length of text input allowed.
minLengthMinimum length of text input required.
modelValueThe v-model value of the text field.
nameThe name attribute of the input element.
patternPattern for input validation using regex.
placeholderPlaceholder text shown when input is empty.
readonlyWhether the field is readonly.
requiredWhether the field is required.
schemaSchema for field validation.
typeThe type of input field (text, password, email, etc).
valueThe value attribute of the input element.

Returns

These are the properties in the object returned by the useTextField composable.

NameTypeDescription
descriptionPropsProps for the description element.
displayErrorDisplay the error message for the field.
errorMessageThe error message for the field.
errorMessageProps
Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }>
Props for the error message element.
errorsThe errors for the field.
fieldValueThe value of the field.
inputElReference to the input element.
inputPropsProps for the input element.
isDirtyWhether the field is dirty.
isDisabledWhether the field is disabled.
isTouchedWhether the field is touched.
isValidWhether the field is valid.
labelPropsProps for the label element.
setErrors
(messages: Arrayable<string>) => void
Sets the errors for the field.
setTouchedSets the touched state for the field.
setValueSets the value for the field.
validityDetailsValidity details for the input element.