SearchField

A text field for search queries, with a search icon and a clear button

Usage

<script setup lang="ts">
import { SearchField } from '@hareui/vue'
</script>
<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <SearchField class="w-[280px]" name="search" label="Search" placeholder="Search..." /></template>

Anatomy

SearchField renders a Label, a group holding a search icon, a native type="search" input and a clear button, then a Description and a FieldError. The text parts come from props; each can be replaced with a slot, and the icons can be replaced too.

<template>
  <SearchField label="…" description="…" error-message="…" :invalid="…">
    <template #label />      <!-- Label content -->
    <template #icon />       <!-- search icon content (default: magnifier icon) -->
    <template #clearIcon />  <!-- clear button icon content (default: × icon) -->
    <template #description />  <!-- Description content -->
    <template #error="{ isInvalid, validationErrors, validationDetails }" /> <!-- FieldError content, rendered while invalid -->
  </SearchField>
</template>
ui keydata-slotElement
basesearch-field<div> root
—labelLabel (iff label / #label)
groupsearch-field-group<div> wrapping the icon, input and clear button
searchIconsearch-field-search-icon<span> (default icon or #icon)
inputsearch-field-input<input type="search">
clearButtonsearch-field-clear-buttonCloseButton (default icon or #clearIcon), hidden via CSS while empty
—descriptionDescription (iff description / #description)
—field-errorFieldError (while invalid, iff error-message / #error / validation errors)

Examples

In Surface

When used inside a surface (HeroUI's Surface, here a bg-surface container), use variant="secondary" to apply the lower emphasis variant suitable for surface backgrounds.

Enter keywords to search
Use filters to refine your search
<script setup lang="ts">import { SearchField, Surface } from '@hareui/vue'</script><template>  <Surface class="flex w-full max-w-sm flex-col gap-4 rounded-3xl p-6">    <SearchField name="search" variant="secondary" label="Search" placeholder="Search..." description="Enter keywords to search" :ui="{ input: 'w-full' }" />    <SearchField name="search-2" variant="secondary" label="Advanced search" placeholder="Advanced search..." description="Use filters to refine your search" :ui="{ input: 'w-full' }" />  </Surface></template>

With Description

Enter keywords to search for products
Search by name, email, or username
<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <div class="flex flex-col gap-4">    <SearchField class="w-[280px]" name="search" label="Search products" placeholder="Search products..." description="Enter keywords to search for products" />    <SearchField class="w-[280px]" name="search-users" label="Search users" placeholder="Search users..." description="Search by name, email, or username" />  </div></template>

Required Field

Minimum 3 characters required
<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <div class="flex flex-col gap-4">    <SearchField required class="w-[280px]" name="search" label="Search" placeholder="Search..." />    <SearchField required class="w-[280px]" name="search-query" label="Search query" placeholder="Enter search query..." description="Minimum 3 characters required" />  </div></template>

Disabled State

This search field is disabled
This search field is disabled
<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <div class="flex flex-col gap-4">    <SearchField disabled class="w-[280px]" name="search" model-value="Disabled search" label="Search" description="This search field is disabled" />    <SearchField disabled class="w-[280px]" name="search-empty" label="Search" description="This search field is disabled" />  </div></template>

Full Width

<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <div class="w-[400px] space-y-4">    <SearchField full-width name="search" label="Search" placeholder="Search..." />  </div></template>

Validation

Use invalid together with error-message to control the state yourself, or let the field validate: native attributes (required, …) and a validate function, inside a Form or on their own. Without error-message, the field shows the validation errors.

<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <div class="flex flex-col gap-4">    <SearchField invalid required class="w-[280px]" name="search" model-value="ab" label="Search" error-message="Search query must be at least 3 characters" />    <SearchField invalid class="w-[280px]" name="search-invalid" model-value="invalid@query" label="Search" placeholder="Search..." error-message="Invalid characters in search query" />  </div></template>

Controlled

Bind the value with v-model to synchronize counters, previews, or formatting. The clear button and Escape both empty it; pressing Enter emits submit with the current value.

Current value: (empty)
<script setup lang="ts">import { Button, SearchField } from '@hareui/vue'import { ref } from 'vue'const value = ref('')</script><template>  <div class="flex flex-col gap-4">    <SearchField v-model="value" class="w-[280px]" name="search" label="Search">      <template #description>        Current value: {{ value || '(empty)' }}      </template>    </SearchField>    <div class="flex gap-2">      <Button variant="tertiary" @click="value = ''">        Clear      </Button>      <Button variant="tertiary" @click="value = 'example query'">        Set example      </Button>    </div>  </div></template>

Form Example

Enter at least 3 characters to search
<script setup lang="ts">import { Button, Form, SearchField, Spinner } from '@hareui/vue'import { computed, ref } from 'vue'const value = ref('')const isSubmitting = ref(false)const MIN_LENGTH = 3const isInvalid = computed(() => value.value.length > 0 && value.value.length < MIN_LENGTH)function handleSubmit(e: Event) {  e.preventDefault()  if (value.value.length < MIN_LENGTH)    return  isSubmitting.value = true  // Simulate API call  setTimeout(() => {    console.log('Search submitted:', { query: value.value })    value.value = ''    isSubmitting.value = false  }, 1500)}</script><template>  <Form class="flex w-[280px] flex-col gap-4" @submit="handleSubmit">    <SearchField      v-model="value"      required      :invalid="isInvalid"      full-width      name="search"      label="Search products"      placeholder="Search products..."      :error-message="`Search query must be at least ${MIN_LENGTH} characters`"      :description="`Enter at least ${MIN_LENGTH} characters to search`"    />    <Button class="w-full" :disabled="value.length < MIN_LENGTH" :pending="isSubmitting" type="submit" variant="primary">      <template v-if="isSubmitting">        <Spinner color="current" size="sm" />        Searching...      </template>      <template v-else>        Search      </template>    </Button>  </Form></template>

With Validation

Enter at least 3 characters to search
<script setup lang="ts">import { SearchField } from '@hareui/vue'import { computed, ref } from 'vue'const value = ref('')const isInvalid = computed(() => value.value.length > 0 && value.value.length < 3)</script><template>  <div class="flex flex-col gap-4">    <SearchField      v-model="value"      required      :invalid="isInvalid"      class="w-[280px]"      name="search"      label="Search"      placeholder="Search..."      error-message="Search query must be at least 3 characters"      description="Enter at least 3 characters to search"    />  </div></template>

Variants

<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <div class="flex flex-col gap-4">    <SearchField class="w-[280px]" name="primary-search" variant="primary" label="Primary variant" placeholder="Search..." />    <SearchField class="w-[280px]" name="secondary-search" variant="secondary" label="Secondary variant" placeholder="Search..." />  </div></template>

With Keyboard Shortcut

Add keyboard shortcuts to quickly focus the search field.

Use keyboard shortcut to quickly focus this field
Press⇧ S to focus the search field
<script setup lang="ts">import { Kbd, SearchField } from '@hareui/vue'import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue'const value = ref('')const field = useTemplateRef<{ $el: HTMLElement }>('field')const input = () => field.value?.$el.querySelector('input') ?? nullfunction handleKeyDown(e: KeyboardEvent) {  // Check for Shift+S  if (e.shiftKey && e.key === 'S' && !e.metaKey && !e.ctrlKey && !e.altKey) {    e.preventDefault()    input()?.focus()  }  // Check for ESC key to blur the input  if (e.key === 'Escape' && document.activeElement === input())    input()?.blur()}onMounted(() => window.addEventListener('keydown', handleKeyDown))onBeforeUnmount(() => window.removeEventListener('keydown', handleKeyDown))</script><template>  <div class="flex flex-col gap-4">    <div>      <SearchField        ref="field"        v-model="value"        name="search"        label="Search"        placeholder="Search..."        description="Use keyboard shortcut to quickly focus this field"        :ui="{ input: 'w-[280px]' }"      />    </div>    <div class="text-default-500 flex items-center gap-2 text-sm">      <span>Press</span>      <Kbd keys="shift">        S      </Kbd>      <span>to focus the search field</span>    </div>  </div></template>

Render Function

HeroUI's render prop replaces the root element. In Vue, attributes you pass to SearchField (other than aria-label / aria-labelledby, which go to the input) fall through to the root div.

<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <!-- Attributes other than aria-label/aria-labelledby fall through to the root element (HeroUI's `render` prop) -->  <SearchField class="w-[280px]" data-custom="foo" name="search" label="Search" placeholder="Search..." /></template>

Customization

Tailwind CSS

Use the icon and clearIcon slots to render custom icons, and :ui or class for styling.

Custom icon children
<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <div class="flex flex-col gap-4">    <SearchField name="search-custom" label="Search (Custom Icons)" description="Custom icon children">      <template #icon>        <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">          <path clip-rule="evenodd" d="M12.5 4c0 .174-.071.513-.885.888S9.538 5.5 8 5.5s-2.799-.237-3.615-.612C3.57 4.513 3.5 4.174 3.5 4s.071-.513.885-.888S6.462 2.5 8 2.5s2.799.237 3.615.612c.814.375.885.714.885.888m-1.448 2.66C10.158 6.888 9.115 7 8 7s-2.158-.113-3.052-.34l1.98 2.905c.21.308.322.672.322 1.044v3.37q.088.02.25.021c.422 0 .749-.14.95-.316c.185-.162.3-.38.3-.684v-2.39c0-.373.112-.737.322-1.045zM8 1c3.314 0 6 1 6 3a3.24 3.24 0 0 1-.563 1.826l-3.125 4.584a.35.35 0 0 0-.062.2V13c0 1.5-1.25 2.5-2.75 2.5s-1.75-1-1.75-1v-3.89a.35.35 0 0 0-.061-.2L2.563 5.826A3.24 3.24 0 0 1 2 4c0-2 2.686-3 6-3m-.88 12.936q-.015-.008-.013-.01z" fill="currentColor" fill-rule="evenodd" />        </svg>      </template>      <template #clearIcon>        <svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">          <path clip-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14M6.53 5.47a.75.75 0 0 0-1.06 1.06L6.94 8L5.47 9.47a.75.75 0 1 0 1.06 1.06L8 9.06l1.47 1.47a.75.75 0 1 0 1.06-1.06L9.06 8l1.47-1.47a.75.75 0 1 0-1.06-1.06L8 6.94z" fill="currentColor" fill-rule="evenodd" />        </svg>      </template>    </SearchField>  </div></template>
<script setup lang="ts">import { SearchField } from '@hareui/vue'</script><template>  <SearchField    class="w-full max-w-64"    name="docs"    variant="secondary"    label="Search docs"    placeholder="Components, guides..."    :ui="{ group: 'rounded-xl bg-default', searchIcon: 'text-muted', input: 'placeholder:text-muted', clearButton: 'text-muted' }"  /></template>

Global Configuration

app.use(createHareUI({
  ui: { searchField: { slots: { base: 'gap-2' } } },
}))

Styling Reference

Slots

  • base → [data-slot="search-field"] – root container (flex flex-col gap-1)
  • group → [data-slot="search-field-group"] – bordered field shell (inline-flex h-9 items-center rounded-field border)
  • searchIcon → [data-slot="search-field-search-icon"] – icon wrapper (size-4 text-field-placeholder)
  • input → [data-slot="search-field-input"] – the native input
  • clearButton → [data-slot="search-field-clear-button"] – CloseButton (size-5)

Note: The child components (Label, Description, FieldError) have their own themes. See their respective pages for customization options.

Interactive States

SearchField sets these data attributes:

  • Invalid: [data-invalid="true"] on the root and the group - automatically hides the description when invalid
  • Disabled: [data-disabled="true"] on the root and the group
  • Required: [data-required="true"] on the root - shows the label's required asterisk
  • Read Only: [data-readonly="true"] on the root
  • Empty: [data-empty="true"] on the root - hides the clear button (pointer-events-none opacity-0) while there's no value
  • Focus Within: [data-focus-within="true"] on the group - applied when the input is focused
  • Focus Visible: [data-focus-visible="true"] on the group - applied when focus is visible (keyboard navigation)

API Reference

Props

PropTypeDefaultDescription
labelstring-Label text rendered above the field.
descriptionstring-Helper text rendered below the field. Hidden while the field is invalid.
errorMessagestring-Error message rendered below the field while invalid. Defaults to the validation errors.
idstringuseId()The id of the input; label, description and error ids derive from it.
namestring-The name of the input, used when submitting a form.
placeholderstring-Temporary text shown when the input is empty.
defaultValuestring-The initial value when uncontrolled (no v-model).
variant"primary" | "secondary"'primary'Visual variant of the field.
fullWidthbooleanfalseWhether the field takes the full width of its container.
disabledbooleanfalseWhether the field is disabled.
invalidbooleanundefinedWhether the field is invalid. Overrides validation when set.
requiredbooleanfalseWhether the field is required.
readonlybooleanfalseWhether the input can be selected but not changed.
validateValidateFn<string>-Validates the value. Return an error message (or several) when invalid, true/null/undefined when valid.
validationBehaviorValidationBehavior'native'native blocks form submission and shows errors on change or submit; aria shows errors in realtime. Defaults to the surrounding Form.
uiComponentSlots<{ slots: { base: string[]; group: string[]; input: string[]; searchIcon: string; clearButton: string; }; variants: { variant: { primary: { base: string; }; secondary: { group: string[]; }; }; fullWidth: { false: { base: string; group: string; }; true: { base: string; group: string; }; }; }; defaultVariants: { fullWidth: boolean; variant: string; }; }>-Per-slot class overrides.
modelValuestring-The search query.

Slots

SlotPropsDescription
labelanyLabel content. Replaces the label prop.
iconanyReplaces the default search icon.
clearIconanyReplaces the default clear (×) icon inside the clear button.
descriptionanyDescription content. Replaces the description prop.
errorValidationResultError content, rendered while invalid. Replaces the errorMessage prop.

Emits

EventPayloadDescription
update:modelValueunknown[]-
clear[]-
submit[value: string]-

Accessibility

  • The label's for points to the input id (auto-generated with useId() unless id is set)
  • The input's aria-describedby lists the ids of the rendered description (<id>-description) and error (<id>-error)
  • invalid sets aria-invalid="true" on the input; the error is announced via role="alert"
  • required and disabled set the native attributes on the input
  • The clear button is excluded from the tab order (tabindex="-1") and labeled "Clear search"; clicking it refocuses the input instead of moving focus away
  • Escape clears the field (and does nothing when it's already empty); Enter emits submit with the current value

On this page

No Headings