Skip to content
touchcn
Esc
navigateopen⌘Jpreview
On this page

Searchbar

A search field with a leading magnifier and clear control.

A search input. iOS renders a rounded gray field with a Cancel button that expands on focus; Material renders a full-width pill on a high tonal surface. A clear (×) button appears while the field has a value, and pressing Enter fires the search event.

Installation

npx touchcn add searchbar

Usage

<tcn-searchbar [(value)]="query" placeholder="Search" (search)="onSearch($event)" />
import { TcnSearchbar } from '@/components/ui/searchbar';

<TcnSearchbar value={query} onValueChange={setQuery} placeholder="Search" onSearch={onSearch} />
<script setup lang="ts">
import { TcnSearchbar } from '@/components/ui/searchbar';
</script>

<template>
  <TcnSearchbar v-model="query" placeholder="Search" @search="onSearch" />
</template>

Props

Prop Type Default Description
value string '' Field value (two-way on Angular).
onValueChange (value: string) => void Value callback (React).
placeholder string 'Search' Placeholder text.
cancelLabel string 'Cancel' Label of the iOS Cancel button.
search / onSearch (value: string) => void Fired when Enter is pressed.
Source · Angular
export * from './tcn-searchbar';
import { Component, ElementRef, input, model, output, viewChild } from '@angular/core';

/**
 * A search input. iOS renders a rounded gray field with a leading magnifier and
 * a Cancel button that expands on focus; MD3 renders a full-width pill on a high
 * tonal surface. A clear (×) button appears while the field has a value.
 * Pressing Enter emits `search`.
 */
@Component({
  selector: 'tcn-searchbar',
  template: `
    <form class="tcn-searchbar-wrap flex items-center" (submit)="onSubmit($event)">
      <div class="tcn-searchbar relative flex flex-1 items-center gap-2 px-3">
        <svg class="tcn-searchbar-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
          <circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
          <path d="m20 20-3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
        </svg>
        <input
          #input
          type="search"
          class="tcn-searchbar-input min-w-0 flex-1 bg-transparent outline-none"
          [placeholder]="placeholder()"
          [value]="value()"
          (input)="value.set($any($event.target).value)"
        />
        @if (value()) {
          <button
            type="button"
            class="tcn-searchbar-clear"
            aria-label="Clear"
            (mousedown)="$event.preventDefault()"
            (click)="clear()"
          >
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <circle cx="12" cy="12" r="10" fill="currentColor" />
              <path d="m9 9 6 6m0-6-6 6" stroke="var(--color-surface-container)" stroke-width="2" stroke-linecap="round" />
            </svg>
          </button>
        }
      </div>
      <button
        type="button"
        class="tcn-searchbar-cancel if-ios"
        (mousedown)="$event.preventDefault()"
        (click)="cancel()"
      >
        {{ cancelLabel() }}
      </button>
    </form>
  `,
  host: { class: 'block' },
})
export class TcnSearchbar {
  readonly value = model('');
  readonly placeholder = input('Search');
  readonly cancelLabel = input('Cancel');
  readonly search = output<string>();
  readonly cancelled = output<void>();

  private readonly input = viewChild<ElementRef<HTMLInputElement>>('input');

  protected onSubmit(event: Event): void {
    event.preventDefault();
    this.search.emit(this.value());
  }

  protected clear(): void {
    this.value.set('');
  }

  protected cancel(): void {
    this.value.set('');
    this.cancelled.emit();
    this.input()?.nativeElement.blur();
  }
}
Source · React
export * from './tcn-searchbar';
import { useRef } from 'react';
import type { FormEvent } from 'react';

export interface TcnSearchbarProps {
  value: string;
  onValueChange(value: string): void;
  placeholder?: string;
  cancelLabel?: string;
  /** Fired when Enter is pressed. */
  onSearch?(value: string): void;
  /** Fired when the iOS Cancel button is pressed. */
  onCancel?(): void;
}

/**
 * A search input. iOS renders a rounded gray field with a leading magnifier and
 * a Cancel button that expands on focus; MD3 renders a full-width pill on a high
 * tonal surface. A clear (×) button appears while the field has a value.
 * Pressing Enter fires `onSearch`.
 */
export function TcnSearchbar({
  value,
  onValueChange,
  placeholder = 'Search',
  cancelLabel = 'Cancel',
  onSearch,
  onCancel,
}: TcnSearchbarProps) {
  const inputRef = useRef<HTMLInputElement>(null);

  const handleSubmit = (event: FormEvent) => {
    event.preventDefault();
    onSearch?.(value);
  };

  const cancel = () => {
    onValueChange('');
    onCancel?.();
    inputRef.current?.blur();
  };

  return (
    <form className="tcn-searchbar-wrap flex items-center" onSubmit={handleSubmit}>
      <div className="tcn-searchbar relative flex flex-1 items-center gap-2 px-3">
        <svg className="tcn-searchbar-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
          <circle cx="11" cy="11" r="7" stroke="currentColor" strokeWidth="2" />
          <path d="m20 20-3-3" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
        </svg>
        <input
          ref={inputRef}
          type="search"
          className="tcn-searchbar-input min-w-0 flex-1 bg-transparent outline-none"
          placeholder={placeholder}
          value={value}
          onChange={(event) => onValueChange(event.target.value)}
        />
        {value && (
          <button
            type="button"
            className="tcn-searchbar-clear"
            aria-label="Clear"
            onMouseDown={(event) => event.preventDefault()}
            onClick={() => onValueChange('')}
          >
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
              <circle cx="12" cy="12" r="10" fill="currentColor" />
              <path d="m9 9 6 6m0-6-6 6" stroke="var(--color-surface-container)" strokeWidth="2" strokeLinecap="round" />
            </svg>
          </button>
        )}
      </div>
      <button
        type="button"
        className="tcn-searchbar-cancel if-ios"
        onMouseDown={(event) => event.preventDefault()}
        onClick={cancel}
      >
        {cancelLabel}
      </button>
    </form>
  );
}
Source · Vue
<script setup lang="ts">
import { ref } from 'vue';

withDefaults(defineProps<{ placeholder?: string; cancelLabel?: string }>(), {
  placeholder: 'Search',
  cancelLabel: 'Cancel',
});

const emit = defineEmits<{
  /** Fired when Enter is pressed. */
  search: [value: string];
  /** Fired when the iOS Cancel button is pressed. */
  cancel: [];
}>();

/** Two-way bound search text (`v-model`). */
const model = defineModel<string>({ default: '' });

const inputEl = ref<HTMLInputElement | null>(null);

/**
 * A search input. iOS renders a rounded gray field with a leading magnifier and
 * a Cancel button that expands on focus; MD3 renders a full-width pill on a high
 * tonal surface. A clear (×) button appears while the field has a value.
 * Pressing Enter fires `search`.
 */
const handleSubmit = (): void => {
  emit('search', model.value);
};

const cancel = (): void => {
  model.value = '';
  emit('cancel');
  inputEl.value?.blur();
};
</script>

<template>
  <form class="tcn-searchbar-wrap flex items-center" @submit.prevent="handleSubmit">
    <div class="tcn-searchbar relative flex flex-1 items-center gap-2 px-3">
      <svg class="tcn-searchbar-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
        <circle cx="11" cy="11" r="7" stroke="currentColor" stroke-width="2" />
        <path d="m20 20-3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
      </svg>
      <input
        ref="inputEl"
        v-model="model"
        type="search"
        class="tcn-searchbar-input min-w-0 flex-1 bg-transparent outline-none"
        :placeholder="placeholder"
      />
      <button
        v-if="model"
        type="button"
        class="tcn-searchbar-clear"
        aria-label="Clear"
        @mousedown.prevent
        @click="model = ''"
      >
        <svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
          <circle cx="12" cy="12" r="10" fill="currentColor" />
          <path d="m9 9 6 6m0-6-6 6" stroke="var(--color-surface-container)" stroke-width="2" stroke-linecap="round" />
        </svg>
      </button>
    </div>
    <button type="button" class="tcn-searchbar-cancel if-ios" @mousedown.prevent @click="cancel">
      {{ cancelLabel }}
    </button>
  </form>
</template>
export { default as TcnSearchbar } from './TcnSearchbar.vue';

Last updated on July 24, 2026

Was this page helpful?