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
Section titled “Features”- Uses
input
ortextarea
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
Section titled “Anatomy”Building a Text Field Component
Section titled “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 { 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>
<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
Section titled “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 { 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>
<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
Section titled “Validation”HTML Constraints
Section titled “HTML Constraints”You can use the following properties to validate the text field with native HTML constraint validation:
Name | Type | Description |
---|---|---|
maxLength | number | The maximum length of characters. |
minLength | number | The minimum length of characters. |
required | boolean | Whether the text field is required. |
pattern | string | RegExp | A 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.
Type | Description |
---|---|
email | Validates the value as an email. |
url | Validates 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
Section titled “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
Section titled “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.
Disabled
Section titled “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
Section titled “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>
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
Section titled “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.
These are the properties that can be passed to the useTextField
composable.
Name | Type | Description |
---|---|---|
autocomplete | Autocomplete hint for the input field. | |
description | Description text that provides additional context about the field. | |
disabled | Whether the field is disabled. | |
disableHtmlValidation | Whether to disable HTML5 validation. | |
label | The label of the text field. | |
maxLength | Maximum length of text input allowed. | |
minLength | Minimum length of text input required. | |
modelValue | The v-model value of the text field. | |
name | The name attribute of the input element. | |
pattern | Pattern for input validation using regex. | |
placeholder | Placeholder text shown when input is empty. | |
readonly | Whether the field is readonly. | |
required | Whether the field is required. | |
schema | Schema for field validation. | |
type | The type of input field (text, password, email, etc). | |
value | The value attribute of the input element. |
Returns
Section titled “Returns”These are the properties in the object returned by the useTextField
composable.
Name | Type | Description |
---|---|---|
descriptionProps | Props for the description element. | |
displayError | Display the error message for the field. | |
errorMessage | The error message for the field. | |
errorMessageProps | Ref<{ 'aria-live': "polite"; 'aria-atomic': boolean; id: string; }> | Props for the error message element. |
errors | The errors for the field. | |
fieldValue | The value of the field. | |
inputEl | Reference to the input element. | |
inputProps | Props for the input element. | |
isDirty | Whether the field is dirty. | |
isDisabled | Whether the field is disabled. | |
isTouched | Whether the field is touched. | |
isValid | Whether the field is valid. | |
labelProps | Props for the label element. | |
setErrors | (messages: Arrayable<string>) => void | Sets the errors for the field. |
setTouched | Sets the touched state for the field. | |
setValue | Sets the value for the field. | |
submitErrorMessage | The error message for the field from the last submit attempt. | |
submitErrors | The errors for the field from the last submit attempt. | |
validate | (mutate?: boolean) => Promise<ValidationResult<unknown>> | Validates the field. |
validityDetails | Validity details for the input element. |