Skip to content
Nizivo UI

SearchField

A search or filter input in a <search> landmark, with a magnifier, a clear button and an optional shortcut hint. Inside a <form method="get"> it filters with no JavaScript; inside a Field it is labelled, described and validated by the Field. It is also the top bar's search, as a real input.

Tier: client

Import

import { SearchField } from '@nizivo/react/search-field';

// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/kbd.css
import '@nizivo/react/css/search-field.css';

Static or interactive

Client. It always ships its client leaf, which reads its Field through FieldScope and runs the clear button, so a page with a SearchField also loads FieldScope. The server HTML is a native <search> around an <input type="search"> that works before hydration. How to choose.

In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.

Filtering with no JavaScript

Put it in a <form method="get"> and give it a name. Enter submits the form, so the query lands in the URL (?q=stoneware); the server page reads it from its search params, filters, and passes it back as defaultValue. The <search> element is already the search landmark, so the form needs no role. In a client app, read onChange (or pass value and onChange) and filter as the user types.

Naming it

Inside a Field, the Field's label names the input. Outside one, pass aria-label or aria-labelledby; with neither, the input is named "Search" (labels.label). The placeholder is not a label.

The shortcut

shortcut="mod+k" draws the keys at the end of the field (⌘K on Apple platforms, Ctrl+K elsewhere, through Kbd and ThemeScript's data-nz-platform) and announces them with aria-keyshortcuts ("Meta+K Control+K"). The hint hides while the field has a value. As with IconButton's shortcut, the app binds the key: listen for it and call focus() on the field's ref. A shared hotkey registry comes in a later release.

Size and states

40 px at size="md" (Figma's 42 is off the control scale), 32 at sm, 48 at lg. pending turns the magnifier into a spinner while results load; announce the outcome where the results are. Its edge is border/control, not Figma's border/subtle, whose 1.07:1 against the top bar left the field barely visible.

Examples

Basic

Filtering with no JavaScript: in a <form method="get">, Enter submits the query as ?q=…, and the server page reads it back and passes it as defaultValue. Escape or the clear button empties a filled field.

import type { JSX } from 'react';
import { SearchField } from '@nizivo/react/search-field';

export function SearchFieldExample(): JSX.Element {
  return (
    <form method="get" style={{ maxInlineSize: 360 }}>
      <SearchField name="q" placeholder="Search orders" defaultValue="stoneware" />
    </form>
  );
}

Top Bar

In the top bar: a real input (M49), 360 px wide, with its shortcut drawn at the end. The app binds the key (focus the field through its ref); the input announces it through aria-keyshortcuts.

import type { JSX } from 'react';
import { SearchField } from '@nizivo/react/search-field';

export function SearchFieldTopBarExample(): JSX.Element {
  return (
    <header
      style={{
        display: 'flex',
        alignItems: 'center',
        gap: 16,
        paddingBlock: 12,
        paddingInline: 16,
        background: 'var(--nz-bg-canvas)',
      }}
    >
      <form method="get" style={{ flex: '0 1 360px', minInlineSize: 0 }}>
        <SearchField
          name="q"
          placeholder="Search orders, products, customers"
          shortcut="mod+k"
          labels={{ label: 'Search the store' }}
        />
      </form>
    </header>
  );
}

States

32, 40 and 48 px, the heights of Input, Button and Select; pending and disabled.

import type { JSX } from 'react';
import { SearchField } from '@nizivo/react/search-field';

export function SearchFieldStatesExample(): JSX.Element {
  return (
    <div style={{ display: 'grid', gap: 12, maxInlineSize: 360 }}>
      <SearchField size="sm" aria-label="Search, small" placeholder="Small" />
      <SearchField aria-label="Search, medium" placeholder="Medium" />
      <SearchField size="lg" aria-label="Search, large" placeholder="Large" />
      <SearchField aria-label="Search customers" defaultValue="anna" pending />
      <SearchField aria-label="Search archived orders" placeholder="Archived orders" disabled />
    </div>
  );
}

Field

Inside a Field: the label names it, and the description and error describe it.

By title, SKU or barcode.
Enter at least three characters.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { SearchField } from '@nizivo/react/search-field';

export function SearchFieldFieldExample(): JSX.Element {
  return (
    <div style={{ display: 'grid', gap: 16, maxInlineSize: 360 }}>
      <Field label="Find a product" description="By title, SKU or barcode.">
        <SearchField name="product" placeholder="e.g. Stoneware mug" />
      </Field>
      <Field label="Order number" error="Enter at least three characters.">
        <SearchField name="order" defaultValue="10" />
      </Field>
    </div>
  );
}

Props

Client component Renders data-slot="search-field" with the class nz-search-field. Props: SearchFieldProps. Source: src/search-field/search-field.client.tsx.

NameTypeDefaultDescription
iconReactNode–

Replaces the magnifier before the value (decorative, 16 px).

invalidboolean–

An error message is presented for this field (§8.3). Inside a Field, the Field sets it; an explicit value here wins.

labelsPartial<SearchFieldLabels>–
pendingbooleanfalse

Results are on their way: the magnifier becomes a spinner and the root carries data-pending. Announce the outcome (the number of results) where the results are.

renderRenderProp<SearchFieldState>–

Composition of the root, the <search> box around the native <input>. className and style go on the root too; every other prop, ref included, goes on the <input>.

shortcutstring–

A keyboard shortcut such as "mod+k", drawn at the end of the field with Kbd (⌘K on Apple platforms, Ctrl+K elsewhere) and announced through aria-keyshortcuts. The app binds the key itself (focus the field through its ref), as with IconButton's shortcut; hidden while the field has a value.

sizeControlSize
"sm""md""lg"
'md'

32 / 40 / 48 px, as Input, Button and Select (Figma's 42 is off the scale).

  • Also takes every attribute of <input> except children, prefix, size, type. (Omit<ComponentPropsWithRef<"input">, "type" | "size" | "prefix" | "children">)

render takes an element, or, in client code, a function of the props and this state (SearchFieldState):

NameTypeDefaultDescription
disabledrequiredboolean–
filledrequiredboolean–

The input has a value, so the clear button shows.

invalidrequiredboolean–

An error message is presented (the Field's, or the invalid prop).

pendingrequiredboolean–
readOnlyrequiredboolean–
sizerequiredControlSize
"sm""md""lg"
–

Other exports

SearchFieldLabels type

import type { SearchFieldLabels } from '@nizivo/react/search-field'

SearchField's own strings (§16.4): serialisable, English defaults.

NameTypeDefaultDescription
clearrequiredstring'Clear search'

The clear button's name.

labelrequiredstring'Search'

The input's name when nothing else names it: no Field, aria-label or aria-labelledby. The placeholder is not a label.

SearchFieldProps type

import type { SearchFieldProps } from '@nizivo/react/search-field'

The props of SearchField, listed above.

SearchFieldState type

import type { SearchFieldState } from '@nizivo/react/search-field'

The render state of SearchField, listed above.

Data attributes and slots

Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.

SlotRendered byData attributes
search-field
.nz-search-field
SearchField
root (takes render); interactive, focus: within
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-invalid (present or absent) ← invalid
    An error message is currently presented (not "fails constraints"); always the Field's own state, never the engine's
  • data-pending (present or absent) ← pending
    Data lifecycle: pending (§16.5); also an action in flight on Button and SubmitButton
  • data-readonly (present or absent)
    Mirrors the readonly attribute
  • data-size: "sm""md""lg" ← size
    The size prop; values in each component manifest
search-field-clear
.nz-search-field-clear
SearchField
inner element (CSS only)
none
search-field-control
.nz-search-field-control
SearchField
inner element (CSS only)
none
search-field-icon
.nz-search-field-icon
SearchField
inner element (CSS only)
none
search-field-shortcut
.nz-search-field-shortcut
SearchField
inner element (CSS only)
none

CSS hooks

Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.

HookDefaultRead as
--nz-search-field-bg
  • default: var(--nz-bg-subtle)
  • default: var(--nz-bg-surface)
  • background on .nz-search-field
--nz-search-field-bg-disabled--nz-bg-disabled
  • background on .nz-search-field:is([data-disabled], :has( > .nz-search-field-control:disabled))
--nz-search-field-border
  • default: var(--nz-border-control)
  • default: var(--nz-border-subtle)
  • data-invalid: var(--nz-border-critical)
  • border on .nz-search-field
--nz-search-field-border-hover
  • default: var(--nz-border-control-hover)
  • data-invalid: var(--nz-border-critical)
  • border-color on .nz-search-field:hover:not([data-disabled], :has( > .nz-search-field-control:disabled)) @media (hover: hover)
--nz-search-field-fg--nz-text-primary
  • color on .nz-search-field
--nz-search-field-fg-disabled--nz-text-disabled
  • color on .nz-search-field:is([data-disabled], :has( > .nz-search-field-control:disabled))
--nz-search-field-font
  • default: var(--nz-font-body-m)
  • default @media (pointer: coarse): var(--nz-font-body-l)
  • font on .nz-search-field
--nz-search-field-gap--nz-space-2
  • gap on .nz-search-field
--nz-search-field-height
  • default: var(--nz-size-control-md)
  • data-size="lg": var(--nz-size-control-lg)
  • data-size="sm": var(--nz-size-control-sm)
  • block-size on .nz-search-field
--nz-search-field-icon-fg--nz-icon-secondary
  • color on .nz-search-field-icon
--nz-search-field-inline-sizeauto
  • inline-size on .nz-search-field
--nz-search-field-placeholder-fg--nz-text-tertiary
  • color on .nz-search-field-control::placeholder
--nz-search-field-radius
  • default: var(--nz-radius-lg)
  • data-size="sm": var(--nz-radius-md)
  • border-radius on .nz-search-field

Layers: nz.components. Classes: .nz-glyph-spinner, .nz-search-field, .nz-search-field-clear, .nz-search-field-control, .nz-search-field-icon, .nz-search-field-shortcut.

Tokens this stylesheet reads (33)

--nz-bg-disabled, --nz-bg-muted, --nz-bg-pressed, --nz-bg-subtle, --nz-bg-surface, --nz-border-control, --nz-border-control-hover, --nz-border-critical, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-duration-fast, --nz-duration-spin, --nz-easing-standard, --nz-focus-halo, --nz-focus-halo-critical, --nz-font-body-l, --nz-font-body-m, --nz-icon-primary, --nz-icon-secondary, --nz-radius-full, --nz-radius-lg, --nz-radius-md, --nz-size-control-lg, --nz-size-control-md, --nz-size-control-sm, --nz-size-icon-lg, --nz-size-icon-sm, --nz-space-2, --nz-space-3, --nz-text-disabled, --nz-text-primary, --nz-text-tertiary

Accessibility

These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:

  • search-field: draws the outline when the field inside it has focus.

Keyboard

KeysAction
TabMoves focus to the input. The whole box draws the focus outline; the clear button is not a Tab stop.
EnterSubmits the form the field belongs to (the browser's implicit submission), so a GET form puts the query in the URL.
EscapeClears a filled field and goes no further, so an enclosing Dialog stays open; on an empty field it does nothing.

Source: a native <input type="search"> (search-field.client.tsx); its keydown handler clears on Escape in every engine (Firefox has no native clear), and the clear button empties the field through the native value setter and an input event, so onChange reports it. The browser lane (test/browser/search-field.spec.tsx) checks the clear button, Escape, GET submission, form.reset(), the focus outline and inline validation.

Figma

Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.

On this page