Skip to content
kerfjsGitHub

Search

A text field for finding things, with a search icon and a clear button.

SourcePackage @kerf-js/veneerTry it

Preview

tsx
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/veneer

Try it

Change the controls: the preview and its code update together. The address keeps what you set, so you can link to it.

Live preview
Controls
Size
Codeupdates as you change the controls
import { Search } from '@kerf-js/veneer'<Search label="Search projects" placeholder="Name, owner or region" />

Usage

tsx
import { Search } from '@kerf-js/veneer'

Placeholder

Say what can be searched. The label still names the field.

tsx
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.

tsx
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.

tsx
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

Accessibility

Esc
Clears the text
Enter
Submits its form
  • The field sits in a search landmark, 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

KeyOwn keysStates
parts.fieldbg, radiushover, focus, press, active, open, disabled
parts.field.labeltexthover, focus
ts
veneer({  colors: { brand: '#e8590c', ink: '#1b1b1f' },  parts: {    field: { radius: 4, label: { focus: { text: 'brand' } } },  },})

API Reference

PropTypeDefaultDescription
labelstring—The name people read and screen readers announce. Always required, and it floats on the border by default.
hintunknown—A short line of help under the field.
errorunknown—Replaces the hint and marks the field invalid.
iconIconRef—Leading icon: it sweeps with the border.
prefixunknown—Text or an element inside the field before the value, such as $.
suffixunknown—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.
clearablebooleanfalseA clear button (shown once there is a value).
counternumber—Maximum length, shown as a counter.
idstring—The element id. One is made for you if you leave it out.
valuestring—The current value, when you own the state.
disabledbooleanfalseCan't be focused or changed, and doesn't post with a form.
readonlybooleanfalseCan be focused, read and copied, but not changed. It still posts.
placeholderstring—What can be searched. Never the only label.
EventTypeDefaultDescription
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