Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Search Field

A text field for a search term, with an optional icon and a button that clears it.

Rendered by React 19.3.1

Loading React…
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { SearchField } from '@hamur/react/search-field'

export default function SearchFieldExample() {
  const [searched, setSearched] = React.useState<string | null>(null)
  return (
    <div className="stack">
      <Field.Root className="field">
        <Field.Label>Search the docs</Field.Label>
        <SearchField.Root
          className="search-field"
          defaultValue="dialog"
          onSearch={(value: string) => setSearched(value)}
        >
          <SearchField.Icon className="search-field-icon">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <circle cx="7" cy="7" r="4.5" />
              <path d="m10.5 10.5 3 3" />
            </svg>
          </SearchField.Icon>
          <SearchField.Input
            className="input search-field-input"
            placeholder="Components, hooks, guides"
          />
          <SearchField.Clear className="search-field-clear">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <path d="M4 4l8 8M12 4l-8 8" />
            </svg>
          </SearchField.Clear>
        </SearchField.Root>
        <Field.Description className="muted">Enter searches; Escape clears.</Field.Description>
      </Field.Root>
      <p className="muted">
        {searched === null ? 'No search yet' : `Last search: ${searched || '(empty)'}`}
      </p>
    </div>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<SearchField.Root>
  <SearchField.Icon />
  <SearchField.Input />
  <SearchField.Clear />
</SearchField.Root>

Examples

Filter

A controlled value filters a list of countries as it changes; Escape or the Clear empties the field and shows them all again.

SearchFieldFilter.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { SearchField } from '@hamur/react/search-field'

const countries = ['Argentina', 'Brazil', 'Canada', 'Denmark', 'Egypt', 'France', 'Japan', 'Kenya']

export default function SearchFieldFilterExample() {
  const [query, setQuery] = React.useState('')
  const matches = countries.filter((country) =>
    country.toLowerCase().includes(query.trim().toLowerCase()),
  )
  return (
    <div className="stack">
      <Field.Root className="field">
        <Field.Label>Filter countries</Field.Label>
        <SearchField.Root
          className="search-field"
          value={query}
          onValueChange={(next: string) => setQuery(next)}
        >
          <SearchField.Icon className="search-field-icon">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <circle cx="7" cy="7" r="4.5" />
              <path d="m10.5 10.5 3 3" />
            </svg>
          </SearchField.Icon>
          <SearchField.Input
            className="input search-field-input"
            placeholder="Type a name"
          />
          <SearchField.Clear className="search-field-clear">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <path d="M4 4l8 8M12 4l-8 8" />
            </svg>
          </SearchField.Clear>
        </SearchField.Root>
      </Field.Root>
      <p className="muted">
        {matches.length} of {countries.length} countries
      </p>
      <ul className="search-field-list">
        {matches.map((country) => (
          <li key={country}>{country}</li>
        ))}
      </ul>
    </div>
  )
}

Controlled

A controlled value with a Reset button outside the field; each change from typing, the Clear or Escape shows its reason.

SearchFieldControlled.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { SearchField } from '@hamur/react/search-field'

export default function SearchFieldControlledExample() {
  const [value, setValue] = React.useState('headless')
  const [reason, setReason] = React.useState<string | null>(null)
  return (
    <div className="stack">
      <Field.Root className="field">
        <Field.Label>Search packages</Field.Label>
        <SearchField.Root
          className="search-field"
          value={value}
          onValueChange={(next: string, details: { reason: string }) => { setValue(next) setReason(details.reason) }}
        >
          <SearchField.Icon className="search-field-icon">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <circle cx="7" cy="7" r="4.5" />
              <path d="m10.5 10.5 3 3" />
            </svg>
          </SearchField.Icon>
          <SearchField.Input className="input search-field-input" />
          <SearchField.Clear className="search-field-clear">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <path d="M4 4l8 8M12 4l-8 8" />
            </svg>
          </SearchField.Clear>
        </SearchField.Root>
      </Field.Root>
      <div className="row">
        <button
          className="button"
          type="button"
          onClick={() => {
            setValue('headless')
            setReason(null)
          }}
        >
          Reset
        </button>
      </div>
      <p className="muted">Value: {value || '(empty)'}</p>
      <p className="muted">Reason: {reason ?? 'set by the app'}</p>
    </div>
  )
}

Form

A search in a Form with a name and no onSearch: Enter submits the form, and its Reset button clears the field through form-reset.

SearchFieldForm.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { SearchField } from '@hamur/react/search-field'

export default function SearchFieldFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) => {
        const form = details.event.target as HTMLFormElement
        setSubmitted([...new FormData(form)].map(([key, value]) => `${key}=${value}`).join('&'))
      }}
    >
      <Field.Root name="query" className="field">
        <Field.Label>Search the catalogue</Field.Label>
        <SearchField.Root className="search-field">
          <SearchField.Icon className="search-field-icon">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <circle cx="7" cy="7" r="4.5" />
              <path d="m10.5 10.5 3 3" />
            </svg>
          </SearchField.Icon>
          <SearchField.Input
            className="input search-field-input"
            placeholder="Lamps, chairs, rugs"
          />
          <SearchField.Clear className="search-field-clear">
            <svg className="search-field-svg" viewBox="0 0 16 16">
              <path d="M4 4l8 8M12 4l-8 8" />
            </svg>
          </SearchField.Clear>
        </SearchField.Root>
        <Field.Description className="muted">Enter submits the form.</Field.Description>
      </Field.Root>
      <div className="row">
        <button className="button" type="submit">
          Search
        </button>
        <button className="button" type="reset">
          Reset
        </button>
      </div>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

API reference

19 props across 4 parts, in React syntax. className, style, render are listed once, under Shared props.

ClearSearchField.Clear

A button that clears the field and returns focus to the input. Rendered while there is something to clear, out of the tab order unless tabIndex is given. Renders a <button>.

Clear props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
keepMountedWhether the clear button stays mounted while there is nothing to clear, with data-visiblebooleanfalse
disabledWhether the clear button should ignore user interaction.booleanfalse
Clear data attributes
Data attributeDescription
data-visiblePresent while the clear button is shown: the field has something to clear and is not read-only.
data-disabledPresent when the clear button is disabled.
data-loadingPresent while the application is loading results.

IconSearchField.Icon

A decorative search icon, always mounted and hidden from assistive technology. Renders a <span>.

Icon data attributes
Data attributeDescription
data-disabledPresent when the search field is disabled.
data-loadingPresent while the application is loading results.

InputSearchField.Input

The search input, a native input that works with Field as Input does. Its value is the Root's. Renders an <input>.

Input props
PropTypeDefault
typeThe input's type.SearchFieldInputType'text'
inputModeThe virtual keyboard to show."search" | "text" | "none" | "tel" | "url" | "email" | "numeric" | "decimal"'search'
enterKeyHintThe label of the virtual keyboard's Enter key."search" | "enter" | "done" | "go" | "next" | "previous" | "send"'search'
autoCompleteThe autocomplete token list.string'off'
autoCapitalizeThe autocapitalize hint for virtual keyboards.string'off'
autoCorrectThe autocorrect hint for virtual keyboards.string'off'
spellCheckWhether the browser checks the spelling of the value.Booleanishfalse
dirNameThe name under which the form submits the text direction of the value.string—
Input data attributes
Data attributeDescription
data-disabledPresent when the input is disabled.
data-readonlyPresent when the input is read-only.
data-loadingPresent while the application is loading results.
data-validPresent when the input is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the input is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the input has been touched (when wrapped in Field.Root).
data-dirtyPresent when the input's value has changed (when wrapped in Field.Root).
data-filledPresent when the input is filled (when wrapped in Field.Root).
data-focusedPresent when the input is focused (when wrapped in Field.Root).

RootSearchField.Root

Groups the search input, its clear button and its icon, and holds the value. Renders a <div>.

Root props
PropTypeDefault
valueThe value of the field.string—
defaultValueThe value of the field when it is initially rendered, and the value a form reset restores.string''
onValueChangeCallback fired before the value changes: on typing, on a press of the clear button, on Escape((value: string, eventDetails: SearchFieldRootChangeEventDetails) => void)—
onSearchCallback fired when Enter is pressed in the input, also with an empty value.((value: string, eventDetails: { reason: "keyboard"; event: KeyboardEvent; }) => void)—
disabledWhether the input and the clear button should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the value.booleanfalse
loadingWhether the application is loading results.booleanfalse
labelsThe texts of the search field: clear, the clear button's name when it has no aria-label orPartial<SearchFieldLabels>—
Root data attributes
Data attributeDescription
data-filledPresent when the field has a value.
data-disabledPresent when the search field is disabled.
data-readonlyPresent when the search field is read-only.
data-loadingPresent while the application is loading results.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
EnterCalls onSearch with the term, and does not submit the formInput
EnterWithout onSearch, submits the formInput
EscapeClears a filled field; an empty one leaves the key to what is around itInput
EscapeIn a Dialog or a Popover, clears first and closes on the second pressInput
TabSkips the clear button, which is not a Tab stopClear
EnterSpaceWith tabindex="0" on the Clear, clears the field and focuses the InputClear

Accessibility

What the parts set
PartSetsSo that
Inputtype="text" with role="searchbox", or type="search" with its own role; inputmode and enterkeyhint search; no name of its ownthe field is read as a search box with its Field.Label, and a phone keyboard shows a Search key
Cleara button of type button, tabindex="-1", aria-controls the Input's id and the name "Clear search" (labels); never aria-hiddena screen reader can still reach and press it, a pointer press keeps focus in the field, and Tab skips it
Iconaria-hidden="true", always mountedthe glyph is not read beside the field's name
Roota div with data-filled, data-disabled, data-readonly and data-loading; disabled disables the Input and the Clearthe field and its button cannot disagree; a read-only field renders no Clear and ignores Enter and Escape

Focus order

  1. The Input is the only Tab stop; the Clear has tabindex="-1" unless you give it a tabindex.
  2. A pointer press on the Clear keeps focus in the Input, and a clear by any means leaves focus there.
  3. Nothing takes focus on mount, on Escape or on a form reset.