SearchField
Text input tuned for search: <input type="search" role="searchbox"> with an integrated clear button, Escape-to-clear, Enter-to-submit, and Field.Provider OR-merge for disabled / readonly / required / invalid.
Anatomy
<SearchField.Provider bind:value onSubmit={(v) => runSearch(v)}>
<SearchField.Input placeholder="Search…" />
<SearchField.ClearTrigger>×</SearchField.ClearTrigger>
</SearchField.Provider>
Parts
| Part |
Element |
Description |
Provider |
<div> |
Root context. Holds value, flags (disabled/readonly/required/invalid). |
Input |
<input> |
type="search" role="searchbox". Reads OR-merged state from Field. |
ClearTrigger |
<button> |
Clears the value on click. Hidden via data-empty when value is empty. |
Props
Provider
| Prop |
Type |
Default |
Description |
id |
string |
auto |
DOM id. |
value |
string |
'' |
Controlled input value. bind:value supported. |
onValueChange |
(value: string) => void |
— |
Fires on every keystroke. |
onSubmit |
(value: string) => void |
— |
Fires on Enter in the Input. |
onClear |
() => void |
— |
Fires on Escape, ClearTrigger click, or programmatic clear(). |
clearOnEscape |
boolean |
true |
If false, Escape is a no-op (leave dismissal to the surrounding dialog). |
disabled |
boolean |
false |
OR-merged with Field.disabled. |
readonly |
boolean |
false |
OR-merged with Field.readonly. |
required |
boolean |
false |
OR-merged with Field.required. |
invalid |
boolean |
false |
OR-merged with Field.invalid. |
placeholder |
string |
— |
Forwarded to the Input element. |
aria-label |
string |
translated Search |
Accessible name when no Field.Label is present. |
ClearTrigger
| Prop |
Type |
Default |
Description |
id |
string |
auto |
DOM id. |
aria-label |
string |
translated Clear search |
Accessible name. |
ARIA
| Part |
Attribute |
Value |
| Input |
role |
searchbox |
| Input |
type |
search |
| Input |
aria-invalid |
When invalid is true |
| Input |
aria-required |
When required is true |
| Input |
aria-labelledby |
Set by Field.Label when composed inside Field |
| ClearTrigger |
type |
button |
| ClearTrigger |
aria-label |
Translated default unless overridden |
| ClearTrigger |
tabindex |
-1 — not in the tab order (clear via Escape instead) |
Data Attributes
| Part |
Attribute |
Values |
| Provider |
data-search-field |
Always present |
| Provider |
data-empty |
Present when value === '' |
| Provider |
data-focused |
Present while Input has focus |
| Provider |
data-disabled |
Present when disabled (merged) |
| Provider |
data-readonly |
Present when readonly (merged) |
| Provider |
data-required |
Present when required (merged) |
| Provider |
data-invalid |
Present when invalid (merged) |
| Input |
data-search-field-input |
Always present |
| Input |
data-empty / data-focused / … |
Mirrors Provider for direct styling |
| ClearTrigger |
data-search-field-clear-trigger |
Always present |
| ClearTrigger |
data-empty |
Present when value is empty (hook for visibility) |
Keyboard
| Key |
Action |
Enter |
Fires onSubmit(value) |
Escape |
Clears the value (when clearOnEscape), fires onClear + onValueChange |
i18n
| Key |
English |
Spanish |
label |
Search |
Buscar |
clear |
Clear search |
Borrar búsqueda |
Override per-instance via aria-label on Provider / ClearTrigger.
Comparison
| Feature |
Soma |
Radix |
Ark UI |
bits-ui |
react-aria |
| Dedicated component |
✅ |
❌ |
❌ |
❌ |
✅ |
role="searchbox" |
✅ |
— |
— |
— |
✅ |
| Integrated ClearTrigger part |
✅ |
— |
— |
— |
✅ |
| Escape-to-clear |
✅ |
— |
— |
— |
✅ |
| Enter-to-submit callback |
✅ |
— |
— |
— |
✅ |
Field OR-merge (disabled/invalid/…) |
✅ |
— |
— |
— |
⚠️¹ |
Translated default aria-label |
✅ |
— |
— |
— |
❌ |
data-empty on root for clear visibility |
✅ |
— |
— |
— |
❌ |
| Debounce prop |
❌² |
— |
— |
— |
❌ |
¹ react-aria couples SearchField with its own FieldContext; soma participates in the shared Field.Provider that all form primitives use.
² Debouncing belongs on the consumer: onValueChange → your own debounce(value, 300) hook. No built-in, by design.
Usage
Standalone
<script>
import { SearchField } from '$soma/components';
let q = $state('');
</script>
<SearchField.Provider bind:value={q} onSubmit={(v) => goto(`/search?q=${v}`)}>
<SearchField.Input placeholder="Search posts…" />
<SearchField.ClearTrigger>×</SearchField.ClearTrigger>
</SearchField.Provider>
Inside a Field (label + helper + error)
<Field.Provider invalid={!!error}>
<Field.Label>Find a product</Field.Label>
<Field.Control>
<SearchField.Provider bind:value>
<SearchField.Input placeholder="Name, SKU, tag…" />
<SearchField.ClearTrigger>×</SearchField.ClearTrigger>
</SearchField.Provider>
</Field.Control>
<Field.HelperText>Press Enter to search, Escape to clear.</Field.HelperText>
{#if error}<Field.ErrorText>{error}</Field.ErrorText>{/if}
</Field.Provider>
Debounced live search
<script>
import { debounce } from '$lib/util';
let q = $state('');
const runSearch = debounce((v) => fetchResults(v), 300);
</script>
<SearchField.Provider bind:value={q} onValueChange={runSearch}>
<SearchField.Input placeholder="Live search…" />
<SearchField.ClearTrigger>×</SearchField.ClearTrigger>
</SearchField.Provider>