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.
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, passsearchas an object of these props plusopen,onOpenandonClose: the Search button callsonOpen, and whileopenthe field takes the links' place with Close search after it. Escape on an empty field callsonClose.
Motion: the results use Popover's drop-in and drop-out; the clear button fades with motion-fast.
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
labelstringvaluestringplaceholderstringvalue starts the words; when the page changes it, the field follows.onChange(text: string) => voidonSubmit(text: string) => voidresults{ title?: string; options: PickerOption[]; }[]onSelect(option: PickerOption) => voidbusybooleanbusyText ('Searching').busyTextstringemptyTextstringonClear() => voidonClose() => voidopenbooleanautoFocusbooleanmaxWidthnumberidstringclassNamestringTypes
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; }
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-sizeicon-size-smsearch-clear-rightinset-xssearch-control-padding-rightsearch-results-max-heightsearch-said-paddingsearch-nav-max-widthsearch-nav-gapgap-smType
search-said-fonttext-smallChecked 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 callsonClose. Withresultsthe 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, oremptyText. 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.