# Search field

A text field for filtering or searching, with a magnifier before the text, a clear control after it and an optional key hint at the end.

Block: `smbk-search`. Status: candidate. Figma: the Figma file, https://www.figma.com/community/file/1688324308358870630. Desktop and mobile: differ by presentation only.

## Options

| Axis | Values | Default | Meaning |
|---|---|---|---|
| `size` | `lg`, `md`, `sm` | `md` | lg is 48 tall with larger text, for a search that leads a page; md and sm are the input’s two heights, with their padding, icons and text. Small text never goes under 16 px on a phone. |
| `error` | on / off | off | The query is invalid. Draws the danger border, kept while focused, and the error icon. |

Parts: `smbk-search__control`, `smbk-search__clear`, `smbk-search__kbd`

## Markup

```html
<span class="smbk-input smbk-input--size-md smbk-search smbk-search--size-md">
  <i
    class="ph ph-magnifying-glass smbk-icon smbk-icon--size-md smbk-input__icon"
    aria-hidden="true"
  ></i>
  <input
    type="search"
    class="smbk-input__control smbk-search__control"
    placeholder="Search projects..."
    aria-label="Search projects"
  >
  <kbd class="smbk-kbd smbk-kbd--size-sm smbk-search__kbd" aria-hidden="true">⌘ K</kbd>
</span>
<span class="smbk-input smbk-input--size-md smbk-search smbk-search--size-md">
  <i
    class="ph ph-magnifying-glass smbk-icon smbk-icon--size-md smbk-input__icon"
    aria-hidden="true"
  ></i>
  <input
    type="search"
    class="smbk-input__control smbk-search__control"
    aria-label="Search projects, with a query"
    value="website redesign"
  >
  <button type="button" class="smbk-search__clear" aria-label="Clear search">
    <i class="ph ph-x smbk-icon smbk-icon--size-md" aria-hidden="true"></i>
  </button>
</span>
```

## React

```tsx
import { SearchField } from './design-system/components/search-field/search-field.js';

<SearchField size="md" placeholder="Search projects..." aria-label="Search projects" shortcut="⌘ K" />
<SearchField size="md" defaultValue="website redesign" aria-label="Search projects, with a query" onClear={() => {}} />
```

## Stylesheet

```css
/* smbk-search — Search field. Figma "Search Field" 4212:26902.
 * Built on the input: the block always sits on an smbk-input well, and the
 * magnifier is the input's icon part. This stylesheet adds only what a search
 * needs: a clear control, an optional key hint and the large size. The
 * padding, the icon sizes, hover, focus, error, read-only, disabled and the
 * small size are the input's own, so a change to the input reaches the
 * search field. */

.smbk-search__control {
  appearance: none;
}

.smbk-search__control::-webkit-search-cancel-button,
.smbk-search__control::-webkit-search-decoration {
  appearance: none;
}

/* The cross is the field's icon size, inside a 24 px box, the least a press needs; the box reaches past
   the cross into the gap and the padding, so the cross sits where Figma draws it. */
.smbk-search__clear {
  box-sizing: border-box;
  display: inline-flex;
  flex-shrink: 0;
  align-items: center;
  justify-content: center;
  width: var(--smbk-size-target-min);
  height: var(--smbk-size-target-min);
  margin: calc((var(--smbk-size-icon-md) - var(--smbk-size-target-min)) / 2);
  padding: 0;
  border: 0;
  background: none;
  color: var(--smbk-color-text-subtlest);
  cursor: pointer;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
}

.smbk-search__clear > .smbk-icon {
  font-size: var(--smbk-size-icon-md);
}

.smbk-input--size-sm .smbk-search__clear {
  margin: calc((var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2);
}

.smbk-input--size-sm .smbk-search__clear > .smbk-icon {
  font-size: var(--smbk-size-icon-sm);
}

.smbk-search__clear:focus-visible {
  outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
  outline-offset: 0;
  border-radius: var(--smbk-radius-full);
}

.smbk-search:has(.smbk-input__control:disabled) .smbk-search__clear {
  color: var(--smbk-color-text-disabled);
  pointer-events: none;
}

/* ---- large (lg): 48 tall, Body LG, a wider side padding; the icons, the gap, the corner, the clear
   box and the key hint stay the md field's. Two classes, so it wins over the input's md rules
   whichever stylesheet loads first. ---- */

.smbk-input.smbk-search--size-lg {
  height: var(--smbk-size-control-lg);
  padding: 0 var(--smbk-space-inset-xl);
  font-family: var(--smbk-typography-body-lg-font-family);
  font-size: var(--smbk-typography-body-lg-font-size);
  font-weight: var(--smbk-typography-body-lg-font-weight);
  line-height: var(--smbk-typography-body-lg-line-height);
  letter-spacing: var(--smbk-typography-body-lg-letter-spacing);
}

/* The key hint is a small Kbd at the far end. */
.smbk-search__kbd {
  flex-shrink: 0;
}

.smbk-search:has(.smbk-input__control:disabled) .smbk-search__kbd {
  opacity: var(--smbk-opacity-disabled);
}

/* ---- touch: the clear button takes an invisible press area ----
   On a touch screen an invisible area, centred on it, takes the press out to the comfortable target in each direction
   it falls short of it; its look, its focus ring and its place stay as drawn. */

@media (pointer: coarse) {
  .smbk-search__clear {
    position: relative;
  }

  .smbk-search__clear::after {
    content: '';
    position: absolute;
    inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
  }

  /* Right after the value, halfway into the gap: the well's gap less the margin the cross gives back round its icon. */
  .smbk-input > :is(.smbk-input__control, .smbk-combobox__values) + .smbk-search__clear::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-md) + (var(--smbk-size-icon-md) - var(--smbk-size-target-min)) / 2) / -2);
  }

  .smbk-input--size-sm > :is(.smbk-input__control, .smbk-combobox__values) + .smbk-search__clear::after {
    inset-inline-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), (var(--smbk-space-inline-sm) + (var(--smbk-size-icon-sm) - var(--smbk-size-target-min)) / 2) / -2);
  }
}
```

## Tokens it reads

```json
{
  "size": {
    "target": {
      "min": {
        "$type": "dimension",
        "$value": "{{size.24}}",
        "$description": "Smallest tap area allowed (24, WCAG 2.2)."
      },
      "comfortable": {
        "$type": "dimension",
        "$value": "{{size.44}}",
        "$description": "Comfortable touch target (44)."
      }
    },
    "icon": {
      "md": {
        "$type": "dimension",
        "$value": "{{size.20}}",
        "$description": "Icon box, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{size.16}}",
        "$description": "Icon box, small."
      }
    },
    "control": {
      "lg": {
        "$type": "dimension",
        "$value": "{{size.48}}",
        "$description": "Height of a large control: 48 comfortable, 40 compact. A page may use it for its main call to action; apps use md. Dashboard recipe: use compact density; 40 for the page’s one main action and for forms; 32 for toolbars and filters, the size used most; 28 for actions inside table rows and cards. On phones switch to comfortable density, or give small controls an invisible 44 px tap area (size/target/comfortable)."
      }
    }
  },
  "color": {
    "text": {
      "subtlest": {
        "$type": "color",
        "$value": "{{color.grey.600}}",
        "$description": "Quietest text: hints, captions, placeholders."
      },
      "disabled": {
        "$type": "color",
        "$value": "{{color.grey.500}}",
        "$description": "Text in unavailable controls."
      }
    },
    "border": {
      "focus": {
        "$type": "color",
        "$value": "{{color.red.600}}",
        "$description": "Keyboard focus ring on controls."
      }
    }
  },
  "focus": {
    "ring": {
      "width": {
        "$type": "dimension",
        "$value": "{{border-width.2}}",
        "$description": "Thickness of the focus ring."
      }
    }
  },
  "radius": {
    "full": {
      "$type": "dimension",
      "$value": "{{radius.9999}}",
      "$description": "Capsule: pills, chips, switches."
    }
  },
  "space": {
    "inset": {
      "xl": {
        "$type": "dimension",
        "$value": "{{space.16}}",
        "$description": "Padding inside a component, extra large."
      }
    },
    "inline": {
      "md": {
        "$type": "dimension",
        "$value": "{{space.8}}",
        "$description": "Gap between things side by side, medium."
      },
      "sm": {
        "$type": "dimension",
        "$value": "{{space.6}}",
        "$description": "Gap between things side by side, small."
      }
    }
  },
  "typography": {
    "body-lg": {
      "$type": "typography",
      "$value": {
        "fontFamily": "Geist, Geist Fallback, system-ui, sans-serif",
        "fontSize": "20px",
        "fontWeight": 400,
        "lineHeight": 1.5,
        "letterSpacing": "0px"
      }
    }
  },
  "opacity": {
    "disabled": {
      "$type": "number",
      "$value": 0.5,
      "$description": "Fade for disabled pictures and media (50%)."
    }
  }
}
```

## Purpose

A text field whose value filters or searches something. It looks like the input and adds the two things a search needs: a magnifier that says what the field is for, and a clear control that empties it in one press. It can also show the key that jumps to it.

## Anatomy

- The block, always on the input's well: its markup carries the input's classes as well as its own.
- The magnifier, the input's icon part, always present.
- The control part, a native search control, which is also the input's control part.
- In error, the input's error icon, a warning sign after the query.
- The clear part, a cross button at the field's icon size inside a box the size of the least target a press needs, shown when there is something to clear.
- The kbd part, an optional small Kbd at the far end naming the key that jumps to the field, shown while the field is empty.

## When to use

- Filtering a list, a table or a sidebar on the page.
- A site search box.

## When not to use

- For any value that is not a query. Use the [input](input.md).
- To pick a value from a list by typing part of it. Use a [combobox](combobox.md), which suggests matches as the reader types.

## Behavior

Typing filters or searches, as the page decides. The clear control shows only while the field holds text, and the key hint only while it is empty, so the end of the field holds one of them at a time; pressing the clear control empties the field, calls back, and puts focus back in the field. Escape empties a field holding text in every browser, Safari too, and the page hears it as a change; in an empty field Escape is left to the page. The padding, the icon sizes, hover, focus, error, read-only, disabled and the md and sm sizes are the input's own, because the field is built on the input; a change to the input changes the search field too. The large size is the search field's own: 48 tall, with larger text and wider side padding, for a search that leads a page; its icons, clear control and key hint stay the medium field's. A read-only field shows neither the clear control nor the key hint, and Escape leaves it as it is. A disabled field fades its key hint with the media disabled opacity.

The key hint shows whatever the shortcut is, with a space between keys, e.g. "⌘ K". It is only a picture of the keys: the page that shows it must also wire the shortcut, or the hint is a promise the field does not keep.

The clear control is offered only when the field is given `onClear`. A field whose value the page does not control is emptied by the component when cleared; a field whose value the page controls must be emptied by the page in `onClear`. `onChange` passes the browser's own event, as the native field does.

## Accessibility

### What it does for you

- In development, a search field with no name logs a warning in the console.
- The clear control is a real button with an accessible name, "Clear search", and a box of 24 by 24 px, the least a press needs. Pressing it empties the field and puts focus back in the field.
- The browser's own clear control is hidden so there is one, styled, in the same place in every browser.
- The key hint is hidden from assistive technology.
- Escape empties a field holding text in every browser, Safari too, and the page hears it as a change.

### What you must do

- Give it a label like any input; a search landmark around it helps when it is the page's search.
- Change the clear control's name with `clearLabel` for a page in another language.
- When the page wires the shortcut, name it on the control with `aria-keyshortcuts`, which is where a screen reader looks for it. The hint alone is only a picture of the keys.

### How to test

- Type in the field: the clear control appears at its end. Tab to it and press Enter or Space: the field empties and focus is back in it.
- Press Escape in a field holding text: it empties. In an empty field, Escape is left to the page.
- With a screen reader, the field is read with its label as a search field, and the clear control as the button "Clear search". The key hint is not read.
- With text at 200% and the page 320 pixels wide, the text and the clear control stay inside the field.
- In a forced-color mode, the focus border and the text stay visible.

### Keyboard

| Key | What it does |
|---|---|
| Tab | Moves focus to the field, then to the clear control when it shows. |
| Escape | Empties a field holding text. In an empty or read-only field, it is left to the page. |
| Enter or Space | On the clear control, empties the field and puts focus back in it. |

## Usage rules

### Do

- Show results or filter as the person types when the set is small; wait for Enter when a search is expensive.
- Show the key hint only on the one search field the shortcut reaches.

### Don't

- Put a search field inside a form that submits on Enter unless that is what searching does.
