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
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
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>
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 { 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:
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
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.
Name | Type | Description |
---|---|---|
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
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. | |
validityDetails | Validity details for the input element. |