Search Fields
Input elements of type search are text fields designed for the user to enter search queries into. These are functionally identical to text inputs but may be styled differently by the user agent.
Search fields have extra behaviors and use-cases that set them apart from regular text fields. This composable provides the behavior, state, and accessibility implementation for search fields.
A couple of behaviors set this apart from regular text fields:
- The text content can be cleared with the clear button or a keyboard shortcut.
- They are usually used without a parent
form
element and sometimes without asubmit
button. So they can be submitted with theEnter
keyboard key on their own.
You can find more information about the differences here.
Features
- Uses
input[type="search"]
element as a base. - Labeling, descriptions, and error message displays are automatically linked to input and label elements with
aria-*
attributes. - Support for a custom clear button.
- Validation support with native HTML constraint validation or Standard Schema validation.
- Support for
v-model
binding. - Supported Keyboard features:
Key | Description |
---|---|
Esc | Clears the input value. |
↩Enter | If in a form, submits the form. Otherwise, submits the input with the onSubmit event or callback. |
Anatomy
Building a Search Field Component
The useSearchField
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 SearchFieldProps, useSearchField } from '@formwerk/core';
const props = defineProps<SearchFieldProps>();
const { inputProps, labelProps, fieldValue, errorMessage, errorMessageProps, clearBtnProps,} = useSearchField(props);</script>
<template> <div class="search-field" :class="{ 'is-blank': !fieldValue }"> <label v-bind="labelProps">{{ label }}</label> <div class="input-wrapper"> <svg xmlns="http://www.w3.org/2000/svg" fill="#808080" viewBox="0 0 256 256" > <path d="M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z" ></path> </svg>
<input v-bind="inputProps" /> <button class="clear-btn" v-bind="clearBtnProps"> <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 256 256" > <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z" ></path> </svg> </button> </div>
<div v-if="errorMessage" v-bind="errorMessageProps" class="error"> {{ errorMessage }} </div> <div v-else-if="description" v-bind="descriptionProps" class="hint"> {{ description }} </div> </div></template>
105 collapsed lines
<style scoped>.search-field { --color-primary: #10b981; --color-text: #333; --color-hint: #666; --color-border: #d4d4d8; --color-focus: var(--color-primary); --color-error: #f00; --color-hover: #eee;
label { display: block; margin-bottom: 0.25rem; font-size: 14px; font-weight: 500; color: var(--color-text); }
.hint { font-size: 13px; color: var(--color-hint); opacity: 0; transition: opacity 0.3s ease; }
.hint, .error { margin-top: 0.25rem; }
.error { display: none; font-size: 13px; color: #f00; }
.input-wrapper { display: flex; align-items: stretch; width: max-content; padding: 0.4rem; font-size: 13px; color: var(--color-text); border: 1px solid var(--color-border); border-radius: 6px; transition: border-color 0.3s ease;
&:focus-within { outline: none; border-color: var(--color-focus); box-shadow: 0 0 0 1px var(--color-focus); }
input { appearance: none; border: none; outline: none; padding: 0 0 0 0.6rem; background-color: transparent; font-size: 14px; } }
.clear-btn { border: none; background: transparent; display: flex; align-items: center; justify-content: center; color: var(--color-text); cursor: pointer; }
svg { width: 1.25rem; height: 1.25rem; pointer-events: none; }
&:has(:focus) { .hint { opacity: 1; } }
&:has(:user-invalid) { --border-color: var(--color-error); --color-focus: var(--color-error);
.error { display: block; }
.hint { display: none; } }
&.is-blank { .clear-btn { visibility: hidden; } }}</style>
Notice that we imported the SearchFieldProps
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.
Validation
HTML Constraints
You can use the following native HTML validation properties to validate the search field:
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. |
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 SearchField
component like the one shown above, you can use it like this:
<script setup lang="ts">import { ref } from 'vue';import SearchField from './SearchField.vue';
const search = ref('');</script>
<template> <SearchField label="Keywords" placeholder="Search for..." v-model="search" required min-length="3" max-length="8" /></template>
Standard Schema
useSearchField
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 SearchField from './SearchField.vue';
const schema = z.string().min(3).max(8);</script>
<template> <SearchField label="Standard Schema" placeholder="Search for..." :schema="schema" /></template>
Mixed Validation
All search fields created with Formwerk support mixed validation, which means you can use both HTML constraints and Standard Schemas 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 SearchField from './SearchField.vue';
const schema = z.string().min(3).max(8);</script>
<template> <SearchField label="Mixed Validation" placeholder="Search for..." :schema="schema" 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 SearchField from './SearchField.vue';
const schema = z.string().min(3).max(10);</script>
<template> <SearchField label="HTML Validation Disabled" placeholder="Search for..." disable-html-validation :schema="schema" required /></template>
You can also disable it globally for all fields. For more information, check out the Validation guide.
Usage
Disabled
You can disable the search field by setting the disabled
prop to true
. Disabled fields are not editable, will not be validated, and will not be submitted with the form.
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 SearchField from './SearchField.vue';</script>
<template> <SearchField label="Disabled" disabled value="Well..." /></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 SearchField from './SearchField.vue';</script>
<template> <SearchField label="Readonly" value="Can't change this" readonly /></template>
RTL
The search field doesn’t really need 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 SearchField from './SearchField.vue';</script>
<template> <SearchField label="البحث" placeholder="ابحث عن..." dir="rtl" /></template>
Submitting
Search fields can be used without a parent form
element and sometimes without a submit
button. The useSearchField
accepts an onSubmit
callback that is called when the user presses the Enter
key on the field.
<script setup lang="ts">import SearchField from './SearchField.vue';
const onSubmit = (value: string) => { alert(`Searching for: ${value}`);};</script>
<template> <SearchField label="Search" placeholder="Search for..." @submit="onSubmit" /></template>
Note that empty search fields can be submitted, so you might want to validate the field before submitting.
API
Props
These are the properties that can be passed to the useSearchField
composable.
Name | Type | Description |
---|---|---|
clearButtonLabel | The label text for the clear button. | |
description | The description text for the search field. | |
disabled | Whether the search field is disabled. | |
disableHtmlValidation | Whether to disable HTML5 form validation. | |
label | The label text for the search field. | |
maxLength | The maximum length of text allowed in the search field. | |
minLength | The minimum length of text required in the search field. | |
modelValue | The v-model value of the search field. | |
name | The name attribute for the search field input. | |
onSubmit | Handler called when the search field is submitted via the Enter key. | |
pattern | A regular expression pattern that the search field's value must match. | |
placeholder | Placeholder text shown when the search field is empty. | |
readonly | Whether the search field is readonly. | |
required | Whether the search field is required. | |
schema | Schema for search field validation. | |
value | The value attribute of the search field input. |
Returns
These are the properties in the object returned by the useSearchField
composable.
Name | Type | Description |
---|---|---|
clearBtnProps | Ref<{ tabindex: string; type: "button"; ariaLabel: string; onClick(): void; }> | Props for the clear button. |
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 search field. |