Skip to content

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 a submit button. So they can be submitted with the Enter 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:
KeyDescription
Esc
Clears the input value.
Enter
If in a form, submits the form. Otherwise, submits the input with the onSubmit event or callback.

Anatomy

Label
Label
Value
Clear button
Input
Help text
Description or Error Message

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 { ref } from 'vue';
import SearchField from './SearchField.vue';
const search = ref('');
</script>
<template>
<SearchField label="Keywords" placeholder="Search for..." v-model="search" />
Search value: {{ search }}
</template>
<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:

NameTypeDescription
maxLengthnumberThe maximum length of characters.
minLengthnumberThe minimum length of characters.
requiredbooleanWhether the text field is required.
patternstring | RegExpA 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.

NameTypeDescription
clearButtonLabelThe label text for the clear button.
descriptionThe description text for the search field.
disabledWhether the search field is disabled.
disableHtmlValidationWhether to disable HTML5 form validation.
labelThe label text for the search field.
maxLengthThe maximum length of text allowed in the search field.
minLengthThe minimum length of text required in the search field.
modelValueThe v-model value of the search field.
nameThe name attribute for the search field input.
onSubmitHandler called when the search field is submitted via the Enter key.
patternA regular expression pattern that the search field's value must match.
placeholderPlaceholder text shown when the search field is empty.
readonlyWhether the search field is readonly.
requiredWhether the search field is required.
schemaSchema for search field validation.
valueThe value attribute of the search field input.

Returns

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

NameTypeDescription
clearBtnProps
Ref<{ tabindex: string; type: "button"; ariaLabel: string; onClick(): void; }>
Props for the clear button.
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 search field.