JGuang

MultiSelector

MultiSelector · Astryx

Preview
npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0

MultiSelector

A checkbox dropdown for selecting multiple values from a list. Selected items can display as a count, labels, or badges. Use it for filtering or when presenting a finite set of options where multiple choices are needed.

Installation

npm install @astryxdesign/core@0.7.1 @astryxdesign/theme-neutral@0.7.1 @stylexjs/stylex@0.19.0

Requires React 19+, base CSS and a Theme provider. See Setup.

Multi Selector

A checkbox dropdown for selecting multiple values from a list. Selected items can display as a count, labels, or badges. Use it for filtering or when presenting a finite set of options where multiple choices are needed.

Usage guidance

  • Use for a moderate, finite set of options where multiple choices are needed.
  • Enable search filtering when the list exceeds ~15 options.
  • Use renderOption for custom option rows; the checkbox affordance remains owned by MultiSelector.
  • Enable select-all when most users will want all or nearly all options selected.
  • Use inside InputGroup only when the control needs a short prefix or suffix addon as part of one decorated input surface; prefer count or labels trigger display so the group stays single-line.
  • Use variant="secondary" when a multi-selector sits beside secondary buttons or segmented controls, and variant="ghost" when it sits beside ghost buttons. If validation status is needed there, prefer statusVariant="tooltip" so the toolbar height stays compact.
  • Use presentation="adaptive" when the multi-selector should become a bottom sheet on compact touch screens.
  • Avoid: Use for single-value selection; use Selector instead.
  • Avoid: Show more than ~20 options without enabling search.
  • Avoid: Wrap a disabled MultiSelector in Tooltip to explain why it is disabled; disabled triggers swallow the hover events the wrapper needs. Use the disabledMessage prop instead.

Theming API

{
  "targets": [
    {
      "className": "astryx-multi-selector",
      "visualProps": [
        "variant",
        "size",
        "status"
      ],
      "states": [
        "disabled",
        "readonly"
      ]
    },
    {
      "className": "astryx-multi-selector-empty-state"
    },
    {
      "className": "astryx-multi-selector-search"
    },
    {
      "className": "astryx-multi-selector-section-heading"
    },
    {
      "className": "astryx-multi-selector-indicator-icon",
      "states": [
        "state"
      ]
    },
    {
      "className": "astryx-multi-selector-option",
      "visualProps": [
        "size"
      ],
      "states": [
        "select-all",
        "selected",
        "disabled"
      ]
    },
    {
      "className": "astryx-multi-selector-popup"
    }
  ]
}

Multi Selector

Multi-select dropdown with checkboxes for choosing multiple items.

PropTypeDefaultDescription
label *stringLabel text for accessibility.
options *MultiSelectorOptionType[]Array of items: strings, objects with value/label/icon/disabled, dividers, or sections. An option may carry action: one node — an IconButton, a Button, a menu trigger — the caller renders and names. Once any option does, the popup is a grid whose rows pair the option with its action: Up/Down move rows, the inline-end arrow reaches the action, Enter fires it, pointer and touch press it directly, and pressing it never changes the selection.
value *string[]Currently selected values.
onChange *(value: string[], change?: MultiSelectorChange) => voidCalled when the selection changes. A creation (hasCreate) arrives with {type: "create", query} as the second argument and the query appended to value; the caller adds the matching option in that same update. Every other change passes no descriptor, so a one-argument handler keeps working.
changeAction(value: string[]) => void | Promise<void>Async action on change. Fires after onChange.
placeholderstring'Select...'Placeholder text shown when no value is selected.
size'sm' | 'md' | 'lg''md'Size variant for the selector.
variant'input' | 'secondary' | 'ghost''input'Visual trigger style. input is the bordered input treatment for forms; secondary matches the secondary buttons of the active theme (borderless and neutral-filled by default) for navs and toolbars; ghost is borderless and transparent, matching ghost buttons for toolbar usage.
triggerDisplay'count' | 'labels' | 'badges''count'How to display selected items in the trigger.
formatValue(items: {value: string; label: string}[]) => stringFormats the trigger text when triggerDisplay="count" or "labels". Receives the selected items (value plus resolved label); the count is items.length. Not used by triggerDisplay="badges".
maxBadgesnumber3Maximum badges to show before "+N". Only for triggerDisplay="badges".
hasSelectAllbooleanWhether to show a select-all checkbox.
selectAllLabelstring'Select all'Label for the select-all checkbox.
hasSearchbooleanWhether to show a search input for filtering options. As the user types, the match count (or "No results found") is announced to screen readers via a polite live region. The search field has built-in affordances: a leading magnifier icon and, once a query is typed, a trailing clear (✕) button that resets the query and returns focus to the input.
searchPlaceholderstring'Search...'Placeholder text for the search input.
emptyTextReactNode'No options'Content shown in the dropdown panel when there are no options to show, and announced in a polite live region when the panel opens. The announcement is the text this content renders, read from the DOM, so an element is announced as written and aria-hidden parts are left out of both. Not shown while isLoading.
emptySearchTextReactNode'No results found'Content shown in the dropdown panel when a search query matches no options, and announced in a polite live region at the same time. The announcement is the text this content renders, read from the DOM, so an element is announced as written and aria-hidden parts are left out of both.
hasCreatebooleanfalseWith hasSearch, offers a Create "<query>" row first in the list when the typed text equals no option label under the search’s case-insensitive matching. Picking it, or Enter with nothing highlighted, calls onChange with the query appended to value and a {type: "create", query} descriptor, then clears the search; the caller must add an option for the new value in that update. Nothing is offered while isLoading. Without hasSearch it warns in development and offers nothing.
isDisabledbooleanDisables the selector.
isReadOnlybooleanfalseMakes the selector read-only: the selected values stay visible, focusable, and included in form submission, and retain their combobox identity with aria-readonly. The selection surface, clear action, and disclosure indicator are removed. Unlike isDisabled, the control is not dimmed. isDisabled takes precedence when both are set.
htmlNamestringThe HTML name attribute for form submissions. Renders one hidden input per selected value, like a native multi-select.
disabledMessagestringExplains why the selector is disabled. With isDisabled, shows a tooltip on hover/keyboard focus and keeps the trigger focusable via aria-disabled (activation stays blocked). Use this instead of wrapping a disabled MultiSelector in Tooltip. Disabled controls swallow the hover events an external Tooltip needs.
isLabelHiddenbooleanVisually hides the label while keeping it accessible.
descriptionstringHelper text displayed below the label.
isOptionalbooleanMarks the field as optional.
isRequiredbooleanMarks the field as required.
isLoadingbooleanShows a loading spinner in the trigger.
status{type: 'error' | 'warning' | 'success', message?: string}Validation status with an optional message.
statusVariant'attached' | 'detached' | 'tooltip''attached' for input selectors; 'detached' for secondary and ghost selectorsHow the status message is placed relative to the input. attached overlaps directly below the bordered input and is only valid for the input variant; secondary and ghost selectors detach attached status messages by default. Use tooltip for compact toolbar controls.
renderOption(option: MultiSelectorOptionData) => ReactNodeCustom render function for each selectable option in the dropdown. Not called for dividers, sections, or the select-all row.
indicatorPosition'start' | 'end''start'Which edge of the option row carries the checkbox. end pushes it to the far edge of the row, including on the select-all row.
presentation'popover' | 'bottom-sheet' | 'adaptive''popover'How the option list is presented. adaptive uses a bottom sheet on compact touch screens and an anchored popover otherwise.
widthSizeValueWidth of the field (number = pixels, string used as-is, e.g. "100%"). Sizes the whole field (label, control, and status) so they stay aligned.
startIconIconType | ReactNodeIcon displayed at the start of the selector trigger.
hasClearbooleanfalseShows a clear button when values are selected.
isDefaultOpenbooleanfalseWhether the dropdown starts open on mount.
renderTrigger(props: MultiSelectorRenderTriggerProps) => ReactNodeRender the control the panel hangs off — a glyph in a list row, a chip, an icon button — instead of the selector's own field and button. Spread the given props ({ref, id, onClick, onKeyDown, onFocus, aria-haspopup, aria-expanded, aria-controls, aria-busy}) onto it; the listbox is anchored to that control and named by label. The field chrome (Field, status, clear, spinner) is not rendered. Pair with handleRef to open from a keystroke elsewhere.
handleRefReact.Ref<MultiSelectorHandle>Imperative handle with open(), close(), toggle() and isOpen(). Prefer it over mirroring open state in the parent; the selector owns its visibility.
onOpenChange(isOpen: boolean) => voidCalled whenever the panel opens or closes, however it happened: the trigger, the keyboard, a light dismiss, Escape, or the handle.
onClick(event: MouseEvent) => voidClick handler on the trigger (or the renderTrigger control). Runs after the built-in toggle and cannot cancel it.
onFocus(event: FocusEvent) => voidFocus handler on the trigger. Runs before the selector's own focus handling.
onKeyDown(event: KeyboardEvent) => voidKey handler on the trigger button. Runs after the built-in keyboard model and is skipped for keys it handles and cancels.
containerXstyleStyleXStylesStyleX styles for the field container (label, control, and status together). No effect inside InputGroup (style the InputGroup instead) or in a horizontal-labels FormLayout. Root xstyle, className, and style are not accepted.
containerClassNamestringClass name for the field container. Same targeting as containerXstyle.
containerStyleCSSPropertiesInline styles for the field container. Same targeting as containerXstyle.
controlXstyleStyleXStylesStyleX styles for the visual control box: the element that paints the border and background and holds the value, icons, and end controls. Never the trigger button itself.
controlClassNamestringClass name for the visual control box.
controlStyleCSSPropertiesInline styles for the visual control box.

Anatomy

  • Field: Standalone Field shell that provides the label and optional supporting content; omitted inside InputGroup.
  • Trigger: Painted control that displays the current selection or placeholder and opens the selection surface when editable.
  • Icon-rendered start icon: Optional leading semantic icon or icon component rendered through Icon.
  • Caller-rendered start content: Optional arbitrary React content rendered directly at the start of the trigger.
  • Trigger clear button: Shared clear action that removes every selected value when hasClear is enabled.
  • Status icon: Status glyph shown in place of the disclosure indicator for attached or tooltip status.
  • Indicator icon: Trailing chevron shown when status presentation does not replace it; reflects collapsed or expanded state.
  • Search row: Panel header with a borderless search input and optional clear action.
  • Search icon: Leading magnifier rendered through Icon inside the search row.
  • Search clear button: Shared clear action shown in the search row while a query is present.
  • Option row: Selectable row for an option or the optional select-all choice.
  • Option checkbox indicator: CheckboxInput indicator that presents each row’s selected, unselected, or indeterminate state.
  • Option divider: Divider supplied in the public options data to separate adjacent option groups.
  • Section heading: Visible heading for a labeled group of option rows.
  • Empty state: Message shown when the shared panel content has no options or no search matches.
  • Pointer popup: Anchored painted surface that hosts the shared panel content for popover presentation.
  • Touch sheet heading: Heading above the shared panel content in bottom-sheet presentation.
  • Touch sheet: BottomSheet surface that hosts the same panel content for bottom-sheet presentation.

Examples

  • Multi Selector:
  • MultiSelector — Bottom Sheet: Keeps a multi-selection list open in a bottom sheet while choices are toggled.
  • MultiSelector — Column Visibility: Column visibility toggle with hidden label, search, select-all, and selection count.
  • MultiSelector — Form Composition: Two multi-selectors in a form with required/optional states.
  • MultiSelector — Ghost Toolbar: Borderless MultiSelector variant composed with ghost buttons in a toolbar.
  • MultiSelector — Searchable: Multi-select with search filtering and select-all.
  • MultiSelector — Sectioned Permissions: Multi-select with options grouped into labeled sections.