---
title: "SearchField"
description: "A search input with a trailing magnifier on the form input surface, controlled or uncontrolled."
url: https://docs.systhema.app/components/search-field
version: unreleased (main)
docs_index: https://docs.systhema.app/llms.txt
---

`SearchField` is a single-line `type="search"` input with a trailing magnifier icon. It is built like a [select](https://docs.systhema.app/components/form-select.md): a wrapper holds the input and an icon overlay, on the same input tokens, so a search box and a select sit side by side identically. It holds no state and does no searching; you own the value and the submit.

```tsx preview title="SearchField"
import { SearchField } from '@systhemaui/next'

export default function Demo() {
  return (
    <div role="search" style={{ width: '100%', maxWidth: 480 }}>
      <SearchField name="q" placeholder="Search the docs" aria-label="Search the docs" />
    </div>
  )
}
```

## Import

```tsx
import { SearchField } from '@systhemaui/next'
```

In a React app without Next.js, import it from `@systhemaui/react`.

## Examples

### Controlled

Every native `<input>` prop, `value` and `onChange` included, goes to the input, and the ref points at the input too.

```tsx preview title="Filtering a list"
'use client'

import { useState } from 'react'
import { List, ListItem, Paragraph, SearchField } from '@systhemaui/next'

const components = ['Accordion', 'Button', 'Card', 'Carousel', 'Chip', 'Gallery', 'Heading']

export default function Demo() {
  const [query, setQuery] = useState('')
  const matches = components.filter((name) => name.toLowerCase().includes(query.toLowerCase()))

  return (
    <div style={{ display: 'grid', gap: 16, width: '100%', maxWidth: 480 }}>
      <SearchField
        value={query}
        onChange={(event) => setQuery(event.target.value)}
        placeholder="Filter components"
        aria-label="Filter components"
      />
      {matches.length > 0 ? (
        <List>
          {matches.map((name) => (
            <ListItem key={name}>{name}</ListItem>
          ))}
        </List>
      ) : (
        <Paragraph type="small">No component matches “{query}”.</Paragraph>
      )}
    </div>
  )
}
```

### Next to a select

The search field and [`FormSelect`](https://docs.systhema.app/components/form-select.md) share padding, border, radius and type, so a filter bar lines up without extra CSS.

```tsx preview title="Filter bar"
import { FormGroup, FormRow, FormSelect, Option, SearchField } from '@systhemaui/next'

export default function Demo() {
  return (
    <FormRow style={{ width: '100%', maxWidth: 640 }}>
      <FormGroup>
        <SearchField name="filter-q" placeholder="Search posts" aria-label="Search posts" />
      </FormGroup>
      <FormGroup>
        <FormSelect name="filter-category" defaultValue="all" aria-label="Category">
          <Option value="all">All categories</Option>
          <Option value="news">News</Option>
          <Option value="guides">Guides</Option>
        </FormSelect>
      </FormGroup>
    </FormRow>
  )
}
```

### Custom icon

`icon` replaces the default magnifier. Without it, the wrapper renders an empty `.search-field-icon` that the CSS masks with the magnifier glyph in the input's icon color.

```tsx preview title="Custom icon"
import { SearchField } from '@systhemaui/next'

export default function Demo() {
  return (
    <div style={{ width: '100%', maxWidth: 480 }}>
      <SearchField
        name="q-location"
        placeholder="City or postcode"
        aria-label="City or postcode"
        icon={
          <svg
            viewBox="0 -960 960 960"
            width="100%"
            height="100%"
            fill="currentColor"
            focusable="false"
          >
            <path d="M480-480q33 0 56.5-23.5T560-560q0-33-23.5-56.5T480-640q-33 0-56.5 23.5T400-560q0 33 23.5 56.5T480-480Zm0 400Q319-217 239.5-334.5T160-552q0-150 96.5-239T480-880q127 0 223.5 89T800-552q0 100-79.5 217.5T480-80Z" />
          </svg>
        }
      />
    </div>
  )
}
```

The icon sits in an `aria-hidden` span, so it is never announced.

## Props

`className` goes on the wrapper and `inputClassName` on the input; `type` defaults to `'search'`.

<!-- generated:props @systhemaui/react SearchFieldProps -->

| Prop                          | Type        | Default | Description                                                                                                                                                                                                 |
| ----------------------------- | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `className`                   | `string`    | -       | Class applied to the field wrapper (the positioning context).                                                                                                                                               |
| `inputClassName`              | `string`    | -       | Class applied to the inner `<input>`.                                                                                                                                                                       |
| `icon`                        | `ReactNode` | -       | Custom trailing icon. When omitted the wrapper renders an empty `.search-field-icon` span, which the core CSS masks with the default magnifier glyph (mirroring how `.form-select-icon` masks the chevron). |
| …and all `<input>` attributes |             |         |                                                                                                                                                                                                             |

<!-- /generated -->

## HTML and CSS

```html
<form role="search" action="/search">
  <div class="search-field">
    <input class="search-field-input" type="search" name="q" aria-label="Search" />
    <span class="search-field-icon" aria-hidden="true"></span>
  </div>
</form>
```

The icon must follow the input as a sibling (`.search-field-input ~ .search-field-icon`). The CSS also hides the browser's own search decoration and clear button, so the magnifier is the only glyph.

<!-- generated:utilities form -->

| Class                     | Styles                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `form`                    | `& > :not([hidden], input[type="hidden"]) ~ :not([hidden], input[type="hidden"]) { margin-top: var(--form-row-gap-y); }`                                                                                                                                                                                                                                                                                                                                                                                                        |
| `form-row`                | `@media (width < 640px) { & { display: flex; flex-direction: column; row-gap: var(--form-row-gap-y); } }`<br>`@media (width >= 640px) { & { display: flex; flex-wrap: wrap; gap: var(--form-row-gap-x); } }`<br>`@media (width >= 640px) { & > .form-group { flex: 1 1 0%; min-width: 0; } }`<br>`@media (width >= 640px) { & > .form-group[style*="--width"] { flex: 0 0 auto; width: calc(var(--width) - var(--form-row-gap-x) * (1 - var(--width) / 100%)); } }`                                                             |
| `form-group`              | `@media (width >= 640px) { .form-row > .form-group { flex: 1 1 0%; min-width: 0; } }`<br>`@media (width >= 640px) { .form-row > .form-group[style*="--width"] { flex: 0 0 auto; width: calc(var(--width) - var(--form-row-gap-x) * (1 - var(--width) / 100%)); } }`<br>`& > :not([hidden]) ~ :not([hidden]) { margin-top: var(--form-group-gap-y); }`<br>`&:has(:required) .form-label::after { content: ' *'; color: var(--color-form-label-required); }`<br>+ 1 more rule                                                     |
| `form-label`              | `& { display: block; color: var(--color-form-label); }`<br>`& { font-family: var(--font-form-label-font-family); font-size: var(--typography-form-label-font-size); font-weight: var(--font-form-label-font-weight); letter-spacing: var(--typography-form-label-letter-spacing); line-height: var(--typography-form-label-line-height); text-transform: none; text-decoration: none; font-style: var(--font-form-label-font-style); }`<br>+ 1 more rule                                                                        |
| `form-description`        | `& { display: block; color: var(--color-form-description); }`<br>`& { font-family: var(--font-form-description-font-family); font-size: var(--typography-form-description-font-size); font-weight: var(--font-form-description-font-weight); letter-spacing: var(--typography-form-description-letter-spacing); line-height: var(--typography-form-description-line-height); text-transform: none; text-decoration: none; font-style: var(--font-form-description-font-style); }`                                               |
| `form-input`              | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 4 more rules            |
| `form-select`             | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 12 more rules           |
| `form-textarea`           | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 5 more rules            |
| `search-field-input`      | `& { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…`<br>+ 9 more rules            |
| `is-invalid`              | `:is(.form-select:user-invalid ~ .form-select-icon, .form-select.is-invalid ~ .form-select-icon) { color: var(--color-form-input-block-invalid-icon); background-color: var(--color-form-input-block-invalid-icon); }`<br>`:is(.form-group:has(:user-invalid) .form-invalid-message, .form-group:has(.is-invalid) .form-invalid-message) { display: block; }`                                                                                                                                                                   |
| `form-select-wrapper`     | `position: relative;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `form-select-icon`        | `:is(.form-select ~ .form-select-icon) { pointer-events: none; position: absolute; top: 50%; transform: translateY(-50%); object-fit: contain; object-position: center; overflow: visible; line-height: 100%; right: var(--form-input-block-padding-x); width: var(--form-input-block-icon-size); height: var(--form-input-block-icon-size); font-size: var(--form-input-block-icon-size); color: var(--color-form-input-block-default-icon); }`<br>+ 3 more rules                                                              |
| `search-field`            | `position: relative;`                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `search-field-icon`       | `:is(.search-field-input ~ .search-field-icon) { pointer-events: none; position: absolute; top: 50%; transform: translateY(-50%); object-fit: contain; object-position: center; overflow: visible; line-height: 100%; right: var(--form-input-block-padding-x); width: var(--form-input-block-icon-size); height: var(--form-input-block-icon-size); font-size: var(--form-input-block-icon-size); color: var(--color-form-input-block-default-icon); }`<br>+ 2 more rules                                                      |
| `form-inline-input-group` | `& > :not([hidden]) ~ :not([hidden]) { margin-top: var(--form-input-inline-space-y); }`                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `form-radio`              | `& { position: relative; user-select: none; }`<br>`& input[type="radio"], .form-radio  input[type="checkbox"], .form-radio:before { content: ""; position: absolute; left: 0; top: calc(var(--typography-form-inline-line-height) / 2); transform: translateY(-50%); width: var(--form-input-inline-input-size); height: var(--form-input-inline-input-size); }`<br>`& input[type="radio"], .form-radio  input[type="checkbox"] { opacity: 0; cursor: pointer; }`<br>`&:has(:checked):after { opacity: 1; }`<br>+ 13 more rules |
| `form-checkbox`           | `& { position: relative; user-select: none; }`<br>`& input[type="radio"], .form-checkbox  input[type="checkbox"], .form-checkbox:before { content: ""; position: absolute; left: 0; top: calc(var(--typography-form-inline-line-height) / 2); transform: translateY(-50%); width: var(--form-input-inline-input-size); height: var(--form-input-inline-input-size); }`<br>`& input[type="radio"], .form-checkbox  input[type="checkbox"] { opacity: 0; cursor: pointer; }`<br>+ 14 more rules                                   |
| `form-invalid-message`    | `& { display: none; color: var(--color-form-invalid-message); }`<br>`:is(.form-group:has(:user-invalid) .form-invalid-message, .form-group:has(.is-invalid) .form-invalid-message) { display: block; }`                                                                                                                                                                                                                                                                                                                         |
| `form-file-input`         | `& { cursor: pointer; padding: 0; height: calc(var(--form-input-block-padding-y) * 2 + var(--typography-form-block-line-height)); display: flex; align-items: center; }`<br>+ 2 more rules                                                                                                                                                                                                                                                                                                                                      |

<!-- /generated -->

## Next.js

`@systhemaui/next` re-exports `SearchField` from `@systhemaui/react` unchanged. The Posts module's [`ArchiveSearchFilter`](https://docs.systhema.app/components/archive-search-filter.md) is built from `SearchField` and `FormSelect`.

## Accessibility

- `SearchField` renders no label. Give the input an accessible name with `aria-label`, or a [`FormLabel`](https://docs.systhema.app/components/form-controls.md#formlabel) whose `htmlFor` matches its `id`; a placeholder is not a label.
- Wrap it in `<form role="search">` (or a `<search>` element) so assistive technology lists it as a search landmark, and so `Enter` submits.

## Related

- [ArchiveSearchFilter](https://docs.systhema.app/components/archive-search-filter.md)
- [Select](https://docs.systhema.app/components/form-select.md)
- [Form controls](https://docs.systhema.app/components/form-controls.md)
