Search
A text field for finding things, with a search icon and a clear button.
Preview
import { Search, Stack } from '@kerf-js/veneer'export function Sizes() { return ( <Stack gap="group"> <Search label="Search projects" size="sm" /> <Search label="Search projects" /> <Search label="Search projects" size="lg" /> </Stack> )}Installation
npm install @kerf-js/veneerTry it
Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.
import { Search } from '@kerf-js/veneer'<Search label="Search projects" placeholder="Name, owner or region" />Usage
import { Search } from '@kerf-js/veneer'Placeholder
Say what can be searched. The label still names the field.
import { Search, Stack } from '@kerf-js/veneer'export function Placeholder() { return ( <Stack gap="group"> <Search label="Search projects" placeholder="Name, owner or region" /> <Search label="Search projects" variant="classic" placeholder="Name, owner or region" /> </Stack> )}States
A search field can show a hint or an error, and can be disabled or readonly.
import { Grid, Search } from '@kerf-js/veneer'import { Stack, Text } from '@kerf-js/veneer'export function States() { return ( <Grid cols={3} gap="group" > <Cell name="Empty"> <Search label="Search projects" /> </Cell> <Cell name="Filled"> <Search label="Search projects" defaultValue="checkout" /> </Cell> <Cell name="Hint"> <Search label="Search projects" hint="Name, owner or region." /> </Cell> <Cell name="Invalid"> <Search label="Search projects" defaultValue="region:" error="Finish the filter, like region:eu-west." /> </Cell> <Cell name="Disabled"> <Search label="Search projects" defaultValue="checkout" disabled /> </Cell> <Cell name="Read-only"> <Search label="Search projects" defaultValue="checkout" readonly /> </Cell> </Grid> )}const Cell = ({ name, children,}: { name: string children?: unknown}) => ( <Stack gap="tight"> <Text role="caption" subtle > {name} </Text> {children} </Stack>)Filter a list
Try jun or eu-west.
import { derived, state } from '@kerf-js/core'import { Row, Search, Stack, Text } from '@kerf-js/veneer'export function Projects() { const q = state('') const hits = derived(() => { const trimmed = q.value.trim() const s = trimmed.toLowerCase() return PROJECTS.filter((p) => `${p.name} ${p.owner} ${p.region}` .toLowerCase() .includes(s), ).slice(0, 5) }) return ( <Stack gap="group"> <Search label="Search projects" placeholder="Name, owner or region" value={q.value} onInput={(e) => (q.value = e.currentTarget.value)} /> <Stack gap="tight"> {hits.value.map((p) => ( <Row key={p.id} justify="between" > <Text>{p.name}</Text> <Text subtle>{p.region}</Text> </Row> ))} {!hits.value.length && ( <Text subtle>Nothing matches “{q.value}”.</Text> )} </Stack> </Stack> )}type Status = | 'Live' | 'Building' | 'Failed' | 'Paused'const PROJECTS: { id: string name: string owner: string region: string status: Status deploys: number uptime: number latency: number cost: number updated: string}[] = [ { id: 'storefront', name: 'storefront', owner: 'Priya Raman', region: 'eu-west', status: 'Live', deploys: 412, uptime: 99.98, latency: 84, cost: 1240, updated: '2026-10-31', }, { id: 'checkout', name: 'checkout', owner: 'Tomás Oliveira', region: 'eu-west', status: 'Live', deploys: 288, uptime: 99.95, latency: 112, cost: 980, updated: '2026-10-31', }, { id: 'search-api', name: 'search-api', owner: 'Lena Fischer', region: 'us-east', status: 'Building', deploys: 196, uptime: 99.91, latency: 146, cost: 1610, updated: '2026-10-30', }, { id: 'billing', name: 'billing', owner: 'Mara Lindqvist', region: 'us-east', status: 'Live', deploys: 154, uptime: 99.99, latency: 73, cost: 720, updated: '2026-10-29', }, { id: 'mobile-bff', name: 'mobile-bff', owner: 'Sam Okafor', region: 'ap-south', status: 'Failed', deploys: 233, uptime: 99.42, latency: 208, cost: 860, updated: '2026-10-29', }, { id: 'notifier', name: 'notifier', owner: 'Jun Tanaka', region: 'eu-north', status: 'Live', deploys: 97, uptime: 99.97, latency: 58, cost: 310, updated: '2026-10-27', }, { id: 'reports', name: 'reports', owner: 'Noor Haddad', region: 'us-west', status: 'Paused', deploys: 41, uptime: 99.8, latency: 321, cost: 190, updated: '2026-10-22', }, { id: 'edge-cache', name: 'edge-cache', owner: 'Jun Tanaka', region: 'global', status: 'Live', deploys: 318, uptime: 100, latency: 21, cost: 1430, updated: '2026-10-31', }, { id: 'auth', name: 'auth', owner: 'Mara Lindqvist', region: 'eu-west', status: 'Live', deploys: 176, uptime: 99.99, latency: 66, cost: 540, updated: '2026-10-28', }, { id: 'ingest', name: 'ingest', owner: 'Lena Fischer', region: 'us-east', status: 'Live', deploys: 264, uptime: 99.93, latency: 131, cost: 2120, updated: '2026-10-30', },]When to use
Use it for
- Filtering a list as people type
- Finding something across the app
Reach for something else
- CommandPalette, when it runs actions
Accessibility
- Esc
- Clears the text
- Enter
- Submits its form
- The field sits in a
searchlandmark, so people can jump straight to it. - Announce the number of results in a live region when the list changes a lot.
Theming
Restyle it app-wide from veneer() in your kerf config. Every key takes colour, spacing, size, radius, shadow, type and motion; its own keys are listed. A state nests inside: hover: { bg: 'ink' }. Text on a fill you set is contrast-checked at build. How theming works
| Key | Own keys | States |
|---|---|---|
parts.field | bg, radius | hover, focus, press, active, open, disabled |
parts.field.label | text | hover, focus |
veneer({ colors: { brand: '#e8590c', ink: '#1b1b1f' }, parts: { field: { radius: 4, label: { focus: { text: 'brand' } } }, },})API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | The name people read and screen readers announce. Always required, and it floats on the border by default. |
hint | unknown | — | A short line of help under the field. |
error | unknown | — | Replaces the hint and marks the field invalid. |
icon | IconRef | — | Leading icon: it sweeps with the border. |
prefix | unknown | — | Text or an element inside the field before the value, such as $. |
suffix | unknown | — | Text or an element inside the field after the value, such as kg. |
variant | "floating" | "classic" | — | floating puts the label on the border, classic above the field. |
appearance | "line" | — | line draws only the bottom edge. |
size | "sm" | "md" | "lg" | — | sm, md or lg. Each follows the density around it. |
clearable | boolean | false | A clear button (shown once there is a value). |
counter | number | — | Maximum length, shown as a counter. |
id | string | — | The element id. One is made for you if you leave it out. |
value | string | — | The current value, when you own the state. |
disabled | boolean | false | Can't be focused or changed, and doesn't post with a form. |
readonly | boolean | false | Can be focused, read and copied, but not changed. It still posts. |
placeholder | string | — | What can be searched. Never the only label. |
| Event | Type | Default | Description |
|---|---|---|---|
onInput | ((e: InputEvent & { currentTarget: HTMLInputElement & HTMLTextAreaElemen… | — | Called on each keystroke. |
Parts
Every element carries a stable name, so you can style it from your own CSS: [data-scope="search"] [data-part="input"]
input
RelatedInputCommand palette