Skip to content

Select / Multiselect

TIP

If you need more customization for the dropdown menu, refer to intergalactic/popper.

The Select component serves as a wrapper over DropdownMenu with the additional functionality of item selection.

Basic usage

In the simplest case, you can implement the select by passing an array of options. The options array consists of objects with the following fields:

  • value: the value of the selected option.
  • label: the value displayed in the trigger when selecting an option.
  • children: represents nested options displayed in the dropdown list.
tsx
import React from 'react';
import { Flex } from '@semcore/flex-box';
import Select from '@semcore/select';
import { Text } from '@semcore/typography';

const options = Array(6)
  .fill('')
  .map((_, index) => ({
    value: index, // value of the selected option
    label: `Option ${index}`, // the value displayed in the trigger when the option is selected
    children: `Option ${index}`, // option's children displayed in the dropdown
  }));

const Demo = () => (
  <Flex direction='column'>
    <Text tag='label' size={200} htmlFor='basic-select'>
      Basic select
    </Text>
    <Select mt={2} mr='auto' options={options} placeholder='Select option' id='basic-select' />
  </Flex>
);

export default Demo;

Custom selected label

In the label in option item, you could set custom display value for selected option.

tsx
import React from 'react';
import { Flex } from '@semcore/flex-box';
import Select from '@semcore/select';
import { Text } from '@semcore/typography';

const options = Array(6)
  .fill('')
  .map((_, index) => ({
    value: index, // value of the selected option
    label: `Label ${index}`, // the value displayed in the trigger when the option is selected
    children: `Option ${index}`, // option's children displayed in the dropdown
  }));

const Demo = () => (
  <Flex direction='column'>
    <Text tag='label' size={200} htmlFor='select-custom-label'>
      Select with custom selected label
    </Text>
    <Select
      mt={2}
      mr='auto'
      options={options}
      placeholder='Select option'
      id='select-custom-label'
    />
  </Flex>
);

export default Demo;

Controlled and uncontrolled modes

The component can operate in either controlled or uncontrolled mode.

tsx
import React from 'react';
import { Flex } from '@semcore/flex-box';
import Select from '@semcore/select';
import { Text } from '@semcore/typography';

const options = Array(6)
  .fill('')
  .map((_, index) => ({
    value: index,
    label: `Option ${index}`,
    children: `Option ${index}`,
  }));

const { value: initialValue } = options[0];

const Demo = () => {
  const [value, setValue] = React.useState(initialValue);

  return (
    <Flex gap={2} flexWrap>
      <Flex direction='column'>
        <Text tag='label' size={200} htmlFor='controlled-mode-select'>
          Controlled mode
        </Text>
        <Select
          id='controlled-mode-select'
          mt={2}
          value={value}
          onChange={setValue}
          options={options}
          placeholder='Select option'
          m='auto'
          w='100%'
        />
      </Flex>

      <Flex direction='column'>
        <Text tag='label' size={200} htmlFor='uncontrolled-mode-select'>
          Uncontrolled mode
        </Text>
        <Select
          id='uncontrolled-mode-select'
          mt={2}
          defaultValue={initialValue}
          onChange={setValue}
          options={options}
          placeholder='Select option'
          m='auto'
          w='100%'
        />
      </Flex>
    </Flex>
  );
};

export default Demo;

Trigger customization

When you need to customize the trigger, you can pass the desired component to the tag property of the select. The property will be passed to Select.Trigger and replace its render.

tsx
import React from 'react';
import { Flex } from '@semcore/flex-box';
import Select from '@semcore/select';
import { ButtonTrigger, LinkTrigger } from '@semcore/base-trigger';
import { Text } from '@semcore/typography';

const options = Array(6)
  .fill('')
  .map((_, index) => ({
    value: index,
    label: `Option ${index}`,
    children: `Option ${index}`,
  }));

const Demo = () => (
  <Flex gap={4} flexWrap direction='column'>
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='button-trigger-select'>
        Button trigger select
      </Text>
      {/* ButtonTrigger is the default trigger */}
      <Select
        tag={ButtonTrigger}
        options={options}
        id='button-trigger-select'
        placeholder='Select option'
        mt={2}
        mr='auto'
        w='100%'
      />
    </Flex>
    <Flex direction='column'>
      <Select tag={LinkTrigger} options={options} placeholder='Select option' mt={2} mr='auto' />
    </Flex>
  </Flex>
);

export default Demo;

In cases when you require deeper customization, you can "unfold" the component into its constituents. The following example shows how to create a Select component for selecting a list of countries.

tsx
import React from 'react';
import Select from '@semcore/select';
import { Flex } from '@semcore/flex-box';
import Flags, { iso2Name, FlagsIso2 } from '@semcore/flags';
import { Text } from '@semcore/typography';

const formatName = (name?: string) => name?.replace(/([a-z])([A-Z])/g, '$1 $2');
const flags = Object.keys(iso2Name) as FlagsIso2[];

const Demo = () => {
  const [value, setValue] = React.useState<FlagsIso2 | undefined>(undefined);

  return (
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='language-select'>
        Language select
      </Text>
      <Select onChange={(v: FlagsIso2) => setValue(v)} placeholder='Select country'>
        <Select.Trigger mt={2} mr='auto' id='language-select'>
          <Select.Trigger.Addon>
            <Flags iso2={value} />
          </Select.Trigger.Addon>
          <Select.Trigger.Text>{value ? formatName(iso2Name[value]) : ''}</Select.Trigger.Text>
        </Select.Trigger>
        <Select.Menu hMax={180}>
          {flags.map((value) => (
            <Select.Option key={value} value={value}>
              <Flags iso2={value as keyof typeof iso2Name} mr={2} />
              {formatName(iso2Name[value])}
            </Select.Option>
          ))}
        </Select.Menu>
      </Select>
    </Flex>
  );
};

export default Demo;

Similar to intergalactic/dropdown-menu, the dropdown menu can be implemented in two ways:

  • Select.Menu
  • Select.Popper + Select.List

These components serve as wrappers over the corresponding components of the DropdownMenu.

  • Select.Popper is a layout for the dropdown window.
  • Select.List is a component for the option list with the ScrollArea inside.
  • Select.Menu is a wrapper over Select.Popper and Select.List, and all props are passed to Select.List.

This example shows how to insert a Notice in the Select dropdown window.

tsx
import React from 'react';
import Select from '@semcore/select';
import { Flex } from '@semcore/flex-box';
import Notice from '@semcore/notice';
import { Text } from '@semcore/typography';

const options = Array(12)
  .fill('')
  .map((_, index) => `Option ${index}`);

const noticeStyle = {
  border: 'none',
  borderRadius: '0 0 6px 6px',
  padding: '12px 8px',
};

const Demo = () => (
  <Flex direction='column'>
    <Text tag='label' size={200} htmlFor='customized-dropdown-select'>
      Customized dropdown
    </Text>
    <Select placeholder={'Select something'}>
      <Select.Trigger mt={2} mr='auto' id='customized-dropdown-select' />
      <Select.Popper aria-label={'Options and notice'}>
        <Select.List hMax='240px'>
          {options.map((option, index) => (
            <Select.Option value={option} key={index}>
              {option}
            </Select.Option>
          ))}
        </Select.List>
        <Notice style={noticeStyle}>
          <Notice.Content aria-live='polite'>Woooop, it's simple magic!</Notice.Content>
        </Notice>
      </Select.Popper>
    </Select>
  </Flex>
);

export default Demo;

Options

The component offers several variants of options layout:

  • Select.Option: an element of the list (can be selected)
  • Select.Option.Checkbox: a checkbox for an option in a multiselect
  • Select.Option.Hint: a subtitle for an option
  • Select.Group: a group of options, with a title (required) and subTitle (optional)
tsx
import React from 'react';
import { Flex } from '@semcore/flex-box';
import Select from '@semcore/select';
import { Text } from '@semcore/typography';

const Demo = () => (
  <Flex direction='column'>
    <Text tag='label' size={200} htmlFor='options-select'>
      Options
    </Text>
    <Select>
      <Select.Trigger
        placeholder='There are several option types'
        mr='auto'
        mt={2}
        id='options-select'
      />
      <Select.Menu>
        <Select.Option value={1}>Default option</Select.Option>
        <Select.Option value={2}>
          <Select.Option.Checkbox />
          Checkbox option
        </Select.Option>
        <Select.Option value={3} disabled>
          <Select.Option.Checkbox />
          Disabled checkbox option
        </Select.Option>
        <Select.Option value={3}>
          <Select.Option.Content>
            <Select.Option.Checkbox indeterminate />
            Indeterminate checkbox option
          </Select.Option.Content>
          <Select.Option.Hint>Hint for the option</Select.Option.Hint>
        </Select.Option>

        <Select.Group title={'Group title'} subTitle={'Hint for the title'}>
          <Select.Option value={4}>1st option in group</Select.Option>
          <Select.Option value={5}>2nd option in group</Select.Option>
          <Select.Option value={6}>3rd option in group</Select.Option>
        </Select.Group>
      </Select.Menu>
    </Select>
  </Flex>
);

export default Demo;

Options filtering

The InputSearch is added to Select for filtering elements in the list. This is a stylized wrapper over the Input component with a Search icon and a Clear button.

This example shows one of the ways to implement filtering.

tsx
import React from 'react';
import Select from '@semcore/select';
import { ScreenReaderOnly } from '@semcore/flex-box';
import { Text } from '@semcore/typography';
import { Flex } from '@semcore/flex-box';

const Demo = () => {
  const [filter, setFilter] = React.useState('');
  const options = React.useMemo(
    () =>
      data.filter((option) => {
        return option.value.toString().toLowerCase().includes(filter.toLowerCase());
      }),
    [filter],
  );

  return (
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='options-filtering-select'>
        Fruit
      </Text>
      <Select placeholder='Select a fruit'>
        <Select.Trigger id='options-filtering-select' mr='auto' mt={2} />
        <Select.Popper aria-label={'Fruits with search'}>
          <Select.InputSearch
            value={filter}
            onChange={setFilter}
            aria-describedby={filter ? 'search-result' : undefined}
          />
          <Select.List hMax={'224px'}>
            {options.map(({ value, label }) => (
              <Select.Option value={value} key={value}>
                {label}
              </Select.Option>
            ))}
            {options.length ? (
              <ScreenReaderOnly id='search-result' aria-hidden={'true'}>
                {options.length} result{options.length > 1 && 's'} found
              </ScreenReaderOnly>
            ) : (
              <Text
                tag={'div'}
                id='search-result'
                key='Nothing'
                p={'6px 8px'}
                size={200}
                use={'secondary'}
              >
                Nothing found
              </Text>
            )}
          </Select.List>
        </Select.Popper>
      </Select>
    </Flex>
  );
};

const data = [
  'Apple',
  'Banana',
  'Blueberry',
  'Grape',
  'Kiwi',
  'Mango',
  'Melon',
  'Orange',
  'Peach',
  'Pear',
  'Pineapple',
  'Strawberry',
].map((item) => ({
  label: item,
  value: item,
}));

export default Demo;

Advanced filtering control

To get more control over the parts of InputSearch component, you can use the children InputSearch.SearchIcon, InputSearch.Value and InputSearch.Clear components.

In this example the Clear button handler is disabled.

tsx
import React from 'react';
import Select, { InputSearch } from '@semcore/select';
import { ScreenReaderOnly } from '@semcore/flex-box';
import { Text } from '@semcore/typography';
import { Flex } from '@semcore/flex-box';

const Demo = () => {
  const [filter, setFilter] = React.useState('');
  const options = React.useMemo(
    () =>
      data.filter((option) => {
        return option.value.toString().toLowerCase().includes(filter.toLowerCase());
      }),
    [filter],
  );

  return (
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='options-filtering-advanced'>
        Fruit
      </Text>
      <Select placeholder='Select a fruit'>
        <Select.Trigger id='options-filtering-advanced' mr='auto' mt={2} />
        <Select.Popper aria-label={'Fruit options with search'}>
          <InputSearch value={filter} onChange={setFilter}>
            <InputSearch.SearchIcon />
            <InputSearch.Value
              aria-describedby={filter ? 'search-result-advanced' : undefined}
            />
            <InputSearch.Clear
              onClick={() => {
                return false;
              }}
            />
          </InputSearch>
          <Select.List hMax={'224px'}>
            {options.map(({ value, label }) => (
              <Select.Option value={value} key={value}>
                {label}
              </Select.Option>
            ))}
            {options.length ? (
              <ScreenReaderOnly id='search-result-advanced' aria-hidden={'true'}>
                {options.length} result{options.length > 1 && 's'} found
              </ScreenReaderOnly>
            ) : (
              <Select.OptionHint id='search-result-advanced' key='Nothing'>
                Nothing found
              </Select.OptionHint>
            )}
          </Select.List>
        </Select.Popper>
      </Select>
    </Flex>
  );
};

const data = [
  'Apple',
  'Banana',
  'Blueberry',
  'Grape',
  'Kiwi',
  'Mango',
  'Melon',
  'Orange',
  'Peach',
  'Pear',
  'Pineapple',
  'Strawberry',
].map((item) => ({
  label: item,
  value: item,
}));

export default Demo;

Loading state

tsx
import React from 'react';
import { Flex } from '@semcore/flex-box';
import Select from '@semcore/select';
import Spin from '@semcore/spin';
import { Text } from '@semcore/typography';

const Demo = () => (
  <Flex gap={4} flexWrap>
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='loading-select'>
        Normal loading state
      </Text>
      <Select mt={2} mr='auto' id='loading-select'>
        <Select.Trigger loading>Trigger</Select.Trigger>
      </Select>
    </Flex>
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='loading-select-no-chevron'>
        Loading state without chevron
      </Text>
      <div>
        <Select mt={2} mr='auto' id='loading-select-no-chevron'>
          <Select.Trigger chevron={false} placeholder={<Spin size='xs' mx={4} />} tabIndex={-1} />
        </Select>
      </div>
    </Flex>
  </Flex>
);

export default Demo;

Multiselect

The component has the ability to select several options. This functionality can be enabled by using the multiselect property.

The internal layout of options will change to include Select.Option.Checkbox, and the value will become an array.

tsx
import React from 'react';
import { Flex } from '@semcore/flex-box';
import Select from '@semcore/select';
import { Text } from '@semcore/typography';

const options = Array(20)
  .fill('')
  .map((_, index) => ({
    value: index,
    label: `Option ${index}`,
    children: `Option ${index}`,
  }));

const Demo = () => (
  <Flex direction='column'>
    <Text tag='label' size={200} htmlFor='multiselect-select'>
      Multiselect
    </Text>
    <Select mt={2} mr='auto' id='multiselect-select' options={options} multiselect />
  </Flex>
);

export default Demo;

Sorting multiselect options

This example shows one of the ways to sort the selected options.

tsx
import React from 'react';
import Select from '@semcore/select';
import { Text } from '@semcore/typography';
import { Flex } from '@semcore/flex-box';

interface Option {
  value: number;
  title: string;
}

const options = Array(20)
  .fill('')
  .map((i, idx) => ({
    value: idx,
    title: `Awesome option ${idx}`,
  }));

const Option = ({ value, title }: Option) => (
  <Select.Option value={value} key={value}>
    <Select.Option.Checkbox />
    {title}
  </Select.Option>
);

const Demo = () => {
  const [selected, setSelected] = React.useState<number[]>([]);
  const [prevSelected, setPrevSelected] = React.useState<Option[]>([]);

  const handleVisibleChange = (value: boolean) => {
    if (value) return;
    setPrevSelected(options.filter((o) => selected.includes(o.value)));
  };

  const renderOptions = () => {
    if (!prevSelected.length) {
      return options.map((props) => <Option key={props.value} {...props} />);
    }
    const [checked, unchecked] = options.reduce<[Option[], Option[]]>(
      (acc, o) => {
        prevSelected.find((v) => v.value === o.value) ? acc[0].push(o) : acc[1].push(o);
        return acc;
      },
      [[], []],
    );
    return [
      ...checked.map((props) => <Option key={props.value} {...props} />),
      <Select.Divider />,
      ...unchecked.map((props) => <Option key={props.value} {...props} />),
    ];
  };

  return (
    <Flex direction='column'>
      <Text tag='label' size={200} htmlFor='sortable-multiselect'>
        Sortable multiselect
      </Text>
      <Select
        value={selected}
        onChange={(v: number[]) => setSelected(v)}
        onVisibleChange={handleVisibleChange}
        multiselect
        placeholder='Select values'
      >
        <Select.Trigger mt={2} mr='auto' id='sortable-multiselect' />
        <Select.Menu hMax='240px'>{renderOptions()}</Select.Menu>
      </Select>
    </Flex>
  );
};

export default Demo;

Render-function

As with many of our components, you can access the logic of the component by passing a render-function to it.

This example shows how to implement "Select all" and "Deselect all" buttons using this function.

tsx
import React from 'react';
import Select from '@semcore/select';
import { Text } from '@semcore/typography';
import { Flex } from '@semcore/flex-box';

const options = Array(5)
  .fill('')
  .map((i, idx) => ({
    value: `Option ${idx}`,
  }));

const Demo = () => (
  <Flex direction='column'>
    <Text tag='label' size={200} htmlFor='render-function-select'>
      Select with custom render function
    </Text>
    <Select placeholder='Select value' multiselect>
      {(props, handlers) => {
        const {
          getTriggerProps, // function encapsulating Select.Trigger logic
          getPopperProps, // function encapsulating Select.Popper logic
          getListProps, // function encapsulating Select.List logic
          getDividerProps, // function encapsulating Select.Divider logic
          getItemHintProps, // function encapsulating Select.Item.Hint logic
          getItemProps, // function encapsulating Select.Item logic,
          getItemTitleProps, // function encapsulating Select.ItemTitle logic
          getGroupProps, // function encapsulating Select.Group logic
          getOptionProps, // function encapsulating Select.Option logic
          getOptionCheckboxProps, // function encapsulating Select.Option.Checkbox logic
          value: currentValue, // the current value of the select
          visible: currentVisible, // the current value of visibility state
        } = props;
        const {
          visible, // function that controls visibility
          value, // function that controls value
          highlightedIndex, // function that controls the index of the highlighted item
        } = handlers;

        // we manually highlight the first option from the list except 'select all / deselect all'
        React.useEffect(() => {
          if (currentVisible === true) {
            highlightedIndex(1);
          }
        }, [currentVisible]);

        const handleClick = () => {
          const newValue = (currentValue as any).length ? [] : options.map(({ value }) => value);
          value(newValue);

          return false; // cancel the default handler
        };

        return (
          <React.Fragment>
            <Select.Trigger mt={2} mr='auto' id='render-function-select' />
            <Select.Menu>
              <Select.Option value='%all%' onClick={handleClick}>
                <Text color='text-link'>
                  {(currentValue as any).length ? 'Deselect all' : 'Select all'}
                </Text>
              </Select.Option>
              {options.map((option) => (
                <Select.Option value={option.value} key={option.value}>
                  <Select.Option.Checkbox />
                  {option.value}
                </Select.Option>
              ))}
            </Select.Menu>
          </React.Fragment>
        );
      }}
    </Select>
  </Flex>
);

export default Demo;

Last updated:

Released under the MIT License.

Released under the MIT License.