Theme
Sound
Jump to a component
Core · Containers
Picker
Picker is the anchored chooser under a field: optional search, then options in groups.
Live, from @anicanon/sakuga 2.0.0
Sakuga for iOS and Android is coming
iOS
Web only
Picker is made for the web, and isn’t part of Sakuga for iOS yet.
Android
Web only
Picker is made for the web, and isn’t part of Sakuga for Android yet.
- Use it for
- Choosing one thing from many under a field, with search and groups: a cast member, a place, an event.
- Not for
- A short list of plain valuesSelect
- Options that each need an explanation or a priceOptionTiles
- The same choice on a phoneSheet
Guidelines
- Each option has a
label, an optionalhint, and one picture:image(place art, or a portrait withshape: 'round'),initial,media(a picture the product draws: AniCanon™ passesAniCanonKit.EventGlyphfor an event type) oricon. selectedsets the option's label in the strong weight with the check, and no fill;dimis for an option that exists but is out of reach: it can't be chosen.- Group titles say why the options are together ("Already at Sé station"). Put suggestions from the story above plain search results.
openworks as on Popover: it drops in, and plays its exit before it goes.- With sound on, it opens and closes with the surface sounds, and choosing an option plays the choose sound (README · Sound).
- Extra controls that refine the choice (how two events connect) go as children, after the groups.
- The options are a listbox named by
label('Options' when left out); the search field and the children sit outside it. The search field shows no label, sosearch.labelnames it for a screen reader ('Search' when left out).
onClose: as onPopover.search.onChange(text)gets the words as they are typed; the page filtersgroupswith them.onSelect(option)gets the option chosen. With search, focus stays in the field and Up and Down move the active option (Enter chooses it); without search, the options are one Tab stop that Up and Down move through (Enter or Space chooses). A click chooses too.
import { useState } from 'react';
import { Button, Picker } from '@anicanon/sakuga/ui';
export default function PickerExample() {
const [open, setOpen] = useState(true);
return (
<>
<Button
variant="secondary"
size="sm"
icon="plus"
aria-expanded={open ? 'true' : 'false'}
onClick={() => setOpen(!open)}
>
Add someone at Sé station
</Button>
<Picker
open={open}
search={{ value: 'K' }}
groups={[
{
title: 'Matches',
options: [
{ label: 'Kiko', hint: 'E.R.A. · at Sé station', initial: 'K' },
{ label: 'Ken', hint: 'Heros', initial: 'K' },
],
},
{
title: 'Already at Sé station',
options: [
{
label: 'Smith',
hint: 'At Sé station',
initial: 'S',
selected: true,
},
],
},
]}
/>
</>
);
}PropsPickerProps
groupsrequired{ title?: string; options: PickerOption[]; }[]openbooleanAs on Popover.
onClose() => voidAs on Popover.
onSelect(option: PickerOption) => voidAn option chosen: Enter on the active option (with search) or on the focused one (Space too without search), or a click.
labelstringNames the list of options for a screen reader; 'Options' when left out.
search{ placeholder?: string; /** Controlled search text. */ value?: string; /** Initial text for an uncontrolled search. */ defaultValue?: string; /** The search field's name for a screen reader; it shows no label. 'Search' when left out. */ label?: string; /** Called with the text as it is typed; with it the field says it filters the list (aria-autocomplete "list"). */ onChange?: (text: string) => void; }anchoredbooleanstyleCSSPropertieschildrenReactNodeControls that refine the choice, after the groups.
classNamestringTypes
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.
Colour
popover-fgPicker: option label.text-primaryneutral-925#0b0a10text-primaryneutral-0#ffffffpopover-mutedPicker: hints, icons and dim options.text-mutedneutral-600#5c5b67text-mutedneutral-400#a3a3a7popover-groupPicker: group title.text-mutedneutral-600#5c5b67text-mutedneutral-400#a3a3a7popover-slot-bgPicker: initials slot.bg-fill-subtleshade-a06rgba(11,10,16,0.06)bg-fill-subtlewhite-a08rgba(255,255,255,0.08)popover-selected-bgPicker: the selected option.accent-subtlecamellia-600-a10rgba(194,51,95,0.1)accent-subtlecamellia-400-a14rgba(244,119,159,0.14)popover-checkPicker: check on the selected option.text-accentcamellia-700#b42354text-accentcamellia-400#f4779fSpace and shape
picker-list-gapgap-xs4px
picker-search-field-label-marginits own value
calc(-1 * 2px)
picker-group-padding-topinset-sm8px
picker-group-padding-xinset-sm8px
picker-group-padding-bottominset-2xs2px
picker-opt-active-scalescale-press-lg.98
picker-opt-gapgap-md8px
picker-opt-padding-yinset-sm8px
picker-opt-padding-xinset-sm8px
picker-opt-radiusradius-sm8px
picker-opt-img-widthits own value
44px
picker-opt-img-heightits own value
28px
picker-opt-img-radiusradius-xs4px
picker-opt-img-round-sizeits own value
28px
picker-initial-sizeits own value
28px
picker-slot-widthits own value
20px
picker-opt-outline-offsetits own value
calc(-1 * 2px)
picker-slot-icon-sizeicon-size-sm16px
picker-check-icon-sizeicon-size-sm16px
Type
picker-search-field-label-sizeline-hairline1px
Aa
picker-group-fonttext-eyebrow700 11px/14px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
picker-group-letter-spacingtracking-caps.06em
picker-opt-selected-font-weightweight-strong600
picker-opt-fonttext-caption-strong600 13px/18px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
picker-initial-fonttext-micro700 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
picker-label-small-fonttext-small500 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
Motion
picker-opt-durationmotion-fast160ms
picker-opt-easingease-standardcubic-bezier(0.2, 0, 0, 1)
picker-opt-active-durationmotion-instant100ms
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 dialog named by
labelthat, withonClose, takes focus, closes on Escape, a press outside or Tab out of it, and gives focus back (after a press or Tab elsewhere, focus stays there). With search, the field (named "Search", orsearch.label) is a combobox, promising a filtered list only withsearch.onChange, whose active descendant Up and Down move through the options and Enter chooses; without search, the options are one Tab stop that Up and Down move through, Enter or Space choosing; each option reads its label, hint, "out of reach" (unavailable) whendimand "selected", andonSelectgets the choice. - 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.