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';