Sakuga Back to Sakuga
Theme
Sound
Jump to a component

Core · Forms

SearchField

A search: TextField's box with the search icon, a clear button once there are words, and, when the page passes results, Picker's options under it.

@anicanon/sakuga/uiv1.0Used onWeb · iOS · Android

Live, from @anicanon/sakuga 2.0.0

Sakuga for iOS and Android is coming

iOS

Coming soon

SearchField for iOS is in the roughs. The tokens it will read are the ones on this page, the same on every platform.

Android

Coming soon

SearchField for Android is in the roughs. The tokens it will read are the ones on this page, the same on every platform.

Use it for
Finding something by its words: in a series, the library, all of AniCanon™.
Not for
  • Choosing one thing for a field (a cast member for a shot)Picker , whose search is part of the chooser
  • Filtering a list already on screen by a few known valuesSegmentedControl or Chips
  • Writing to MakiMakiComposer

Guidelines

  • The results drop in and leave as Popover does. Group titles say why results are together; put the series' own things above plain matches.
  • In TopNav, pass search as an object of these props plus open, onOpen and onClose: the Search button calls onOpen, and while open the field takes the links' place with Close search after it. Escape on an empty field calls onClose.

Motion: the results use Popover's drop-in and drop-out; the clear button fades with motion-fast.

SearchFieldExample.jsxReact
import { useState } from 'react';
import { SearchField, Text, TopNav } from '@anicanon/sakuga/ui';

const ALL = [
  {
    title: 'Characters',
    options: [
      { label: 'Kiko', hint: 'E.R.A. · Rua da Sé', initial: 'K' },
      { label: 'Ken', hint: 'Heros', initial: 'K' },
    ],
  },
  {
    title: 'Places',
    options: [{ label: 'Kioto Station', hint: 'Night market', icon: 'place' }],
  },
];

function filter(t) {
  t = t.trim().toLowerCase();
  return ALL.map((g) => ({
    title: g.title,
    options: g.options.filter(
      (o) => t && o.label.toLowerCase().indexOf(t) === 0,
    ),
  })).filter((g) => g.options.length);
}

function Live() {
  const [value, setValue] = useState('Ki');
  return (
    <SearchField
      label="Search the series"
      placeholder="Characters, places, episodes"
      value={value}
      results={filter(value)}
      emptyText="Nothing in this series matches"
      onChange={setValue}
      onSelect={(o) => setValue(o.label)}
    />
  );
}

function Nav() {
  const [open, setOpen] = useState(true);
  return (
    <TopNav
      brand={{ name: 'AniCanon' }}
      links={['Home', 'Series', 'Shorts', 'Manga'].map((l) => ({
        label: l,
        current: l === 'Series',
        onClick: () => {},
      }))}
      create={{ onClick: () => {} }}
      account={{ name: 'Raul Menezes' }}
      search={{
        open: open,
        onOpen: () => setOpen(true),
        onClose: () => setOpen(false),
        label: 'Search AniCanon',
        placeholder: 'Series, creators, characters',
      }}
    />
  );
}

function Cap(t) {
  return (
    <Text variant="caption" tone="muted">
      {t}
    </Text>
  );
}

export default function SearchFieldExample() {
  return (
    <>
      {Cap('Rest')}
      <SearchField
        label="Search the series"
        placeholder="Characters, places, episodes"
      />
      {Cap('Typed: clear at the end')}
      <SearchField label="Search the library" value="rooftop chase" />
      {Cap('Results, live')}
      <Live />
      {Cap('Searching')}
      <SearchField label="Search the series" value="Sé" results={[]} busy />
      {Cap('Nothing matches')}
      <SearchField
        label="Search the series"
        value="Zubat"
        results={[]}
        emptyText="Nothing in this series matches"
      />
      {Cap('TopNav, searching')}
      <Nav />
    </>
  );
}

PropsSearchFieldProps

label
string
Names the search and its field; 'Search' when left out. Not drawn.
value
string
The starting words; the field follows when it changes.
placeholder
string
value starts the words; when the page changes it, the field follows.
onChange
(text: string) => void
The words as they are typed.
onSubmit
(text: string) => void
Enter with no result active.
results
{ title?: string; options: PickerOption[]; }[]
Picker's groups; leave out for a search that only submits.
onSelect
(option: PickerOption) => void
busy
boolean
The results are being fetched: the panel says busyText ('Searching').
busyText
string
Said while busy; 'Searching' when left out.
emptyText
string
Said when results has no options; 'Nothing matches' when left out.
onClear
() => void
The clear button was pressed; Escape was pressed with nothing left to close or clear.
onClose
() => void
Escape with nothing left to close or clear.
open
boolean
Starts the results open or closed.
autoFocus
boolean
maxWidth
number
id
string
className
string

Types

interface PickerOption {
    label: string;
    hint?: string;
    image?: string;
    shape?: 'round';
    initial?: string;
    /** A picture the product draws in the option's slot (AniCanon passes an AniCanonKit EventGlyph). */
    media?: ReactNode;
    icon?: IconName;
    selected?: boolean;
    /** Exists but is out of reach: dimmed, said so, and it can't be chosen. */
    dim?: boolean;
}
SearchFieldExample.swiftiOS · coming soon
SearchFieldExample.ktAndroid · coming soon

Each token reads a role, and the role lands on a value in each theme. Change the role and every component that reads it follows.

Space and shape

search-clear-icon-size
icon-size-sm
16px
search-clear-right
inset-xs
4px
search-control-padding-right
its own value
calc(44px + 4px)
search-results-max-height
its own value
360px
search-said-padding
its own value
8px 8px
search-nav-max-width
its own value
560px
search-nav-gap
gap-sm
6px

Type

search-said-font
text-small
500 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif

Checked automatically with axe-core 4.11.4 against WCAG 2.2 A and AA, on the live preview above, in light and dark, on 26 October 2026.

Result
No issues found, in light or in dark.
By design
A search landmark named by label, holding a search field of the same name that Tab reaches. Enter submits the words, or chooses the active result; Escape closes the results, then clears the words, then calls onClose. With results the field is a combobox: Up and Down move its active descendant through the options (Up or Down opens closed results), each option reads as Picker's do, and one status line says how many results there are, that it is searching, or emptyText. The clear button is "Clear search" and gives focus back to the field. The icon is hidden.
Not tested
The automated check doesn't test keyboard use, focus order or what a screen reader says. The lines above are the design system's own account of them.