Sakuga Back to Sakuga
Theme
Sound
Jump to a component

Core · Containers

Picker

Picker is the anchored chooser under a field: optional search, then options in groups.

@anicanon/sakuga/uiv1.0Used onWeb

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 optional hint, and one picture: image (place art, or a portrait with shape: 'round'), initial, media (a picture the product draws: AniCanon™ passes AniCanonKit.EventGlyph for an event type) or icon.
  • selected sets the option's label in the strong weight with the check, and no fill; dim is 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.
  • open works 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, so search.label names it for a screen reader ('Search' when left out).
  • onClose: as on Popover.
  • search.onChange(text) gets the words as they are typed; the page filters groups with 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.
PickerExample.jsxReact
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[]; }[]
open
boolean
As on Popover.
onClose
() => void
As on Popover.
onSelect
(option: PickerOption) => void
An option chosen: Enter on the active option (with search) or on the focused one (Space too without search), or a click.
label
string
Names 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; }
anchored
boolean
style
CSSProperties
children
ReactNode
Controls that refine the choice, after the groups.
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;
}
PickerExample.swiftiOS · coming soon
PickerExample.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.

Colour

popover-fgPicker: option label.
text-primaryneutral-925#0b0a10
text-primaryneutral-0#ffffff
popover-mutedPicker: hints, icons and dim options.
text-mutedneutral-600#5c5b67
text-mutedneutral-400#a3a3a7
popover-groupPicker: group title.
text-mutedneutral-600#5c5b67
text-mutedneutral-400#a3a3a7
popover-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#b42354
text-accentcamellia-400#f4779f

Space and shape

picker-list-gap
gap-xs
4px
picker-search-field-label-margin
its own value
calc(-1 * 2px)
picker-group-padding-top
inset-sm
8px
picker-group-padding-x
inset-sm
8px
picker-group-padding-bottom
inset-2xs
2px
picker-opt-active-scale
scale-press-lg
.98
picker-opt-gap
gap-md
8px
picker-opt-padding-y
inset-sm
8px
picker-opt-padding-x
inset-sm
8px
picker-opt-radius
radius-sm
8px
picker-opt-img-width
its own value
44px
picker-opt-img-height
its own value
28px
picker-opt-img-radius
radius-xs
4px
picker-opt-img-round-size
its own value
28px
picker-initial-size
its own value
28px
picker-slot-width
its own value
20px
picker-opt-outline-offset
its own value
calc(-1 * 2px)
picker-slot-icon-size
icon-size-sm
16px
picker-check-icon-size
icon-size-sm
16px

Type

picker-search-field-label-size
line-hairline
1px
Aa
picker-group-font
text-eyebrow
700 11px/14px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
picker-group-letter-spacing
tracking-caps
.06em
picker-opt-selected-font-weight
weight-strong
600
picker-opt-font
text-caption-strong
600 13px/18px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
picker-initial-font
text-micro
700 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif
picker-label-small-font
text-small
500 12px/16px "Funnel Sans", system-ui, -apple-system, "Segoe UI", sans-serif

Motion

picker-opt-duration
motion-fast
160ms
picker-opt-easing
ease-standard
cubic-bezier(0.2, 0, 0, 1)
picker-opt-active-duration
motion-instant
100ms

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 label that, with onClose, 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", or search.label) is a combobox, promising a filtered list only with search.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) when dim and "selected", and onSelect gets 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.