Selecting a value from a dropdown list of options.

Importing

The component can be imported via:

import { Select } from '@customerio/pluma-components/react';

Usage

To use the Select component, pass in the following props:

  • label - a string label to render above the select
    • alternatively, ariaLabelledby or ariaLabel may be used
  • value - the string value of the currently selected option
  • onChange - a callback to set the value state
  • options - an array of objects to render in the select dropdown menu. The objects should contain the following keys:
    • value - a string value, which corresponds to the value prop
    • label - a string text label to render in the dropdown menu
    • description - optional, a string with additional descriptive text to display below the label
    • searchLabel - optional, a string used for searching the options (if different than the label)
    • icon - a PlumaIcon name to render next to the option
    • isDisabled - optional, whether this options should be disabled (not selectable)
    • tooltip - optional, a string tooltip to show on hover
    • withTruncatedTooltip - optional, automatically show a tooltip when the label is truncated
  • placeholder - a placeholder value to show when no option is selected
import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Choose an option"
		value={value}
		onChange={(value) => setValue(value)}
		options={[
			{ value: 'option_1', label: 'Option 1' },
			{ value: 'option_2', label: 'Option 2' },
			{ value: 'option_3', label: 'Option 3' },
		]}
		placeholder="Choose an option"
	/>
}

Multiple values

A Select component can be used to select multiple values, which can be enabled via the isMulti prop.

When isMulti is true, there are two important differences from the single value case:

  • the value prop is an array of strings (the values of the selected options)
  • the onChange callback is called with an array of strings
import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [values, setValues] = useState(['australian_magpie', 'superb_fairy_wren']);

	return <Select
		label="Your favorite birds"
		value={values}
		onChange={(values) => setValues(values)}
		options={birdOptions}
		placeholder="Select birds"
		isMulti={true}
	/>
}

Options with descriptions

You can add descriptive text below an option's label:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Choose an option"
		value={value}
		onChange={(value) => setValue(value)}
		options={[
			{ 
				value: 'option_1', 
				label: 'Option 1',
				description: 'This is a detailed description for Option 1'
			},
			{ 
				value: 'option_2', 
				label: 'Option 2',
				description: 'A different description for Option 2'
			},
			{ 
				value: 'option_3', 
				label: 'Option 3',
				description: 'Yet another description for Option 3'
			},
		]}
		placeholder="Choose an option"
	/>
}

Disabling options

Individual options can be disabled:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Choose an option"
		value={value}
		onChange={(value) => setValue(value)}
		options={[
			{ value: 'option_1', label: 'Option 1' },
			{ value: 'option_2', label: 'Option 2', isDisabled: true },
			{ value: 'option_3', label: 'Option 3' },
		]}
		placeholder="Choose an option"
	/>
}

Searchable selects

Enabling isSearchable on the component will render a search input inside the select dropdown, which allows users to filter the options:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Your favorite bird"
		value={value}
		onChange={(value) => setValue(value)}
		options={birdOptions}
		placeholder="Select a bird"
		isSearchable={true}
	/>
}

Creating new options

When using a searchable Select, you can allow users to create new options when their search doesn't match any existing options. This is useful for fields like tags, categories, or any user-defined values where the list of options should be dynamic.

Enable this feature with the withCreate and onCreate props:

  • withCreate - enables the "Create new" option when no exact match is found
  • onCreate - callback that receives the search value when "Create new" is selected
  • createOptionLabel - optional custom label for the "Create new" option (default: Create "{value}")
    • this can be a string, or a function which receives the current search string, and returns a string to show
  • canCreateNewOption - optional function to control when the "Create new" option appears
    • can be used to hide the "Create new" option for certain values
import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
  const [value, setValue] = useState(null);
  const [options, setOptions] = useState([...birdOptions]);

  const handleCreate = (label) => {
    const newValue = label.toLowerCase().replace(/s+/g, '_');
    setOptions((prev) => [...prev, { label, value: newValue }].sort((a, b) => a.label.localeCompare(b.label)));
    setValue(newValue);
  };

  return (
    <Select
      label="Favorite Bird"
      placeholder="Select or create a bird"
      options={options}
      value={value}
      onChange={setValue}
      isSearchable
      withCreate
      onCreate={handleCreate}
    />
  );
}

Custom create option label

The label shown for the "Create new" option can be customized using the createOptionLabel prop. It accepts a string or a function that receives the search value:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

const createOptionLabel = (value) => {
	return `Add "${value.trim()}" bird`;
}

export default function Example() {
  const [value, setValue] = useState(null);
  const [options, setOptions] = useState([...birdOptions]);

  const handleCreate = (label) => {
    const newValue = label.toLowerCase().replace(/s+/g, '_');
    setOptions((prev) => [...prev, { label, value: newValue }].sort((a, b) => a.label.localeCompare(b.label)));
    setValue(newValue);
  };



  return (
    <Select
      label="Favorite Bird"
      placeholder="Select or create a bird"
      options={options}
      value={value}
      onChange={setValue}
      isSearchable
      withCreate
      onCreate={handleCreate}
			createOptionLabel={createOptionLabel}
    />
  );
}

Restricting which values can be created

The canCreateNewOption callback can be used to control whether specific values should show/not show the "Create new" option, for example if we want to prevent creation of items that have special names.

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

const canCreateNewOption = (value) => {
	return !['pigeon', 'sparrow', 'duck'].includes(value.trim().toLowerCase());
}

export default function Example() {
  const [value, setValue] = useState(null);
  const [options, setOptions] = useState([...birdOptions]);

  const handleCreate = (label) => {
    const newValue = label.toLowerCase().replace(/s+/g, '_');
    setOptions((prev) => [...prev, { label, value: newValue }].sort((a, b) => a.label.localeCompare(b.label)));
    setValue(newValue);
  };



  return (
    <Select
      label="Favorite Bird"
      placeholder="Select or create a bird"
      options={options}
      value={value}
      onChange={setValue}
      isSearchable
      withCreate
      onCreate={handleCreate}
			canCreateNewOption={canCreateNewOption}
    />
  );
}

Select groups

You can pass in groups of options to the selects to create breaks between sets of options:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Your favorite bird"
		value={value}
		onChange={(value) => setValue(value)}
		options={[
			{
				label: 'Birds',
				options: birdOptions,
			},
			{
				label: 'Animals',
				options: [
					{
						value: 'lion',
						label: 'Lion',
					},
					{
						value: 'tiger',
						label: 'Tiger',
					},
					{
						value: 'bear',
						label: 'Bear'
					}
				]
			}
		]}
		placeholder="Select a creature"
		isSearchable={true}
	/>
}

Disabling groups

An entire group can be disabled by setting its option entry to isDisabled:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Your favorite bird"
		value={value}
		onChange={(value) => setValue(value)}
		options={[
			{
				label: 'Birds',
				options: birdOptions,
				isDisabled: true,
			},
			{
				label: 'Animals',
				options: [
					{
						value: 'lion',
						label: 'Lion',
					},
					{
						value: 'tiger',
						label: 'Tiger',
					},
					{
						value: 'bear',
						label: 'Bear'
					}
				]
			}
		]}
		placeholder="Select a creature"
		isSearchable={true}
	/>
}

Collapsible groups

The Select component supports making groups collapsible - where clicking on the group's header will collapse/expand the group. This is enabled by including an isCollapsible key on the group's object - when set to true, that group will be collapsible.

Additionally, the defaultIsCollapsed key can be used to make a group collapsed by default when first opening the dropdown.

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Your favorite bird"
		value={value}
		onChange={(value) => setValue(value)}
		options={[
			{
				label: 'Birds',
				options: birdOptions,
				isCollapsible: true,
				defaultIsCollapsed: true,
			},
			{
				label: 'Animals',
				isCollapsible: true,
				options: [
					{
						value: 'lion',
						label: 'Lion',
					},
					{
						value: 'tiger',
						label: 'Tiger',
					},
					{
						value: 'bear',
						label: 'Bear'
					}
				]
			}
		]}
		placeholder="Select a creature"
		isSearchable={true}
	/>
}

Virtualized rendering

For very long lists of options (hundreds, thousands of items), rendering the dropdown content may become very slow. For these cases, the Select component supports rendering "virtualized" lists, i.e. it will only render a small subset of options within the dropdown, and will update accordingly as users scroll through it.

For virtualization we use the TanStack Virtual library (both in Ember and React).

This option is disabled by default, and can be enabled via the withVirtualizer argument. Optionally, the virtualizerOptions argument accepts the same options as the library's Virtualizer class, allowing you to customize the behavior if necessary.

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Your favorite item"
		value={value}
		onChange={(value) => setValue(value)}
		options={longOptions}
		placeholder="Select an item"
		isSearchable={true}
		withVirtualizer={true}
	/>
}

Custom value and option components

The Select component accepts valueComponent, optionItemComponent, and optionComponent props, which can be used to customize how the selected value, and the options in the dropdown, are rendered.

To use them, you will need to pass in a component as the prop.

valueComponent is the component that will be used to render the selected value in the Select's trigger. It will be called with the following props:

  • option - the option object from the options array

optionItemComponent is the component that renders each option button in the dropdown. This needs to be customized carefully, as this is the component to which various events and props are attached, to support features like keyboard navigation, focus management, and accessibility via aria props. Pluma also exports a SelectMenuOptionButton component, which is the one used internally for these options. When customizing, it is recommended to use this component as a base to make sure the basic styling and behavior is preserved. This component is called with:

  • option - the option object from the options array
  • isSelected - whether this is the currently selected option (the value matches this option)
  • isActive - whether this is the currently active option (it's currently hovered or focused via keyboard)
  • isDisabled - whether this option is disabled
  • index - the index of this option in the list
  • level - the nesting level of this option
  • collapsibleParentsCount - the number of collapsible parent groups (this, and the one below, are used internally to apply the correct padding to nested options))
  • nonCollapsibleParentsCount - the number of non-collapsible parent groups
  • labelRef - (only in React) a ref to an element inside the option that could become truncated - this is used to detect when an option is truncated to optionally show a tooltip
  • setLabelElement - (only in Ember) like above, but for Ember - a callback to set the potentially truncated element
  • optionComponent - the component that renders this element's contents

optionComponent is the content rendered inside the option button (inside optionItemComponent). It will be called with the following props:

  • option - the option object from the options array
  • isSelected - whether this is the currently selected option (the value matches this option)
  • isActive - whether this is the currently active option (it's currently hovered or focused via keyboard)
  • labelRef - (only in React) a ref to an element inside the option that could become truncated - this is used to detect when an option is truncated to optionally show a tooltip
  • setLabelElement - (only in Ember) like above, but for Ember - a callback to set the potentially truncated element If you intend to enable withTruncatedTooltip on a customized optionComponent, it is required to set the labelRef (in React) or call setLabelElement (in Ember) with the element that might get truncated - otherwise the tooltip won't be able to detect the truncation.

The same component may be used for both valueComponent and optionComponent.

For example, we might define a custom option component like this:

const COLORS = [
	'red',
	'raspberry',
	'clementine',
	'yellow',
	'green',
	'teal',
	'blue',
	'plum',
	'purple',
	'grey',
	'outline',
];

function getIndex(option) {
	return birdOptions.findIndex((o) => o.value === option.value);
}

function CustomOptionComponent({ option, isSelected }) {
	const originalIndex = getIndex(option);

	return (
		<InlineStack alignInline="space-between" alignBlock="center" gap="100">
			<Label 
				color={COLORS[originalIndex % COLORS.length]}
			>
				{option.label}
			</Label>
			{isSelected ? <SelectMenuOptionSelectedIcon /> : null}
		</InlineStack>
	);
}

Which could then be used in the Select component:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Your favorite bird"
		value={value}
		onChange={(value) => setValue(value)}
		options={birdOptions}
		placeholder="Select a bird"
		isSearchable={true}
		valueComponent={CustomOptionComponent}
		optionComponent={CustomOptionComponent}
	/>
}

Custom icon component

You may find that customizing the entire option may be too heavy-handed. Instead, you may just need to customize the icon area that prefixes the option. For such cases, the Select component also accepts an iconComponent prop, which can be used to customize how the icons next to the options are rendered.

To use iconComponent, you will need to pass in a component as the prop.

iconComponent will be called with the following props:

  • name - the name of the icon (this value will be the same as icon on the option)
  • isSelected - whether the option this icon is a part of is currently selected
  • size - the size of the control (which will either be sm or md, as per the size prop)

For example, we might define a custom icon component like this:

export const CustomIconComponent = ({ name, isSelected }) => {
	const color = isSelected ? "success" : "critical";
	
	return (
		<Avatar statusIndicatorColor={color} name={name}>
		</Avatar>
	);
}

Which could then be used in the Select component:

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Your favorite bird"
		value={value}
		onChange={(value) => setValue(value)}
		options={birdsWithIcons}
		placeholder="Select a bird"
		isSearchable={true}
		iconComponent={CustomIconComponent}
	/>
}

Options with tooltips

Individual options can display a tooltip on hover by setting the tooltip property on the option object.

The tooltip placement can be customized with tooltipPlacement (defaults to 'right'), though it's recommended to avoid using top or bottom placements, as they would overlap with other options in the dropdown and make it more difficult to navigate.

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

const options = [
	{ value: 'option_1', label: 'Option 1', tooltip: 'This is the first option' },
	{ value: 'option_2', label: 'Option 2', tooltip: 'This is the second option' },
	{ value: 'option_3', label: 'Option 3', tooltip: 'Tooltip on left', tooltipPlacement: 'left' },
	{ value: 'option_4', label: 'Option 4 (no tooltip)' },
];

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Choose an option"
		value={value}
		onChange={(value) => setValue(value)}
		options={options}
		placeholder="Choose an option"
	/>
}

Truncation tooltips

When options may have long labels that get truncated by CSS, setting withTruncatedTooltip to true on the option will automatically show a tooltip only when the label text is actually truncated. The tooltip content defaults to the option's label, but can be overridden by also setting tooltip.

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

const options = [
	{
		value: 'short',
		label: 'Short label',
		withTruncatedTooltip: true,
	},
	{ 
		value: 'long',
		label: 'This is a very long label that will get truncated',
		withTruncatedTooltip: true,
	},
	{ 
		value: 'custom',
		label: 'Long label with custom tooltip override text',
		withTruncatedTooltip: true,
		tooltip: 'Custom tooltip content',
	},
];

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Options with truncation tooltips"
		value={value}
		onChange={(value) => setValue(value)}
		options={options}
		placeholder="Select an option"
		unsafe_popoverSize={{ maxWidth: 250 }}
	/>
}

Rich tooltip content

When you need tooltips with rich content like links or formatted text, use the optionItemComponent prop to wrap the default SelectMenuOptionButton with a custom Tooltip.

The custom optionItemComponent must be a forwardRef component. When using a custom tooltip, you will also need to combine the external ref with the tooltip's reference using a utility like useMergeRefs.

import { Select, SelectMenuOptionButton, Link, Tooltip, TooltipContent } from '@customerio/pluma-components/react';
import { useMemo, useState, forwardRef } from 'react';

const options = [
	{ value: 'react', label: 'React', docsUrl: 'https://react.dev' },
	{ value: 'vue', label: 'Vue', docsUrl: 'https://vuejs.org' },
	{ value: 'svelte', label: 'Svelte (no tooltip)' },
];

function useMergeRefs(refs) {
	return useMemo(() => {
		return (instance) => {
			refs.forEach((ref) => {
				if (typeof ref === 'function') {
					ref(instance);
				} else if (ref != null) {
					ref.current = instance;
				}
			});
		};
	}, refs);
}

const RichTooltipOptionItem = forwardRef(function RichTooltipOptionItem(props, ref) {
	const { option, ...restProps } = props;

	if (option.docsUrl == null) {
		return <SelectMenuOptionButton ref={ref} option={option} {...restProps} />;
	}

	return (
		<Tooltip isCustom={true} placement="right">
			{(context) => (
				<>
					<SelectMenuOptionButton
						option={option}
						ref={useMergeRefs([ref, context.reference.setReference])}
						{...context.reference.getReferenceProps(restProps)}
					/>
					<TooltipContent>
						See docs here:{' '}
						<Link href={option.docsUrl} isExternal>
							{option.docsUrl}
						</Link>
					</TooltipContent>
				</>
			)}
		</Tooltip>
	);
});

export default function Example() {
	const [value, setValue] = useState(null);

	return <Select
		label="Choose a framework"
		value={value}
		onChange={(value) => setValue(value)}
		options={options}
		optionItemComponent={RichTooltipOptionItem}
		placeholder="Select a framework"
	/>
}

Asynchronous loading

The Select component supports loading options data asynchronously. It will display a loading spinner inside the input when isLoading is true. When performing filtering through an API, you will also have to control the search input state via the searchInputValue and onSearchInput props (otherwise the component will perform filtering internally).

import { Select } from '@customerio/pluma-components/react';
import { useState, useCallback, useRef } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);
	const [searchValue, setSearchValue] = useState('');
	const currentOption = longFlatOptions.find((o) => o.value === value);

	const [options, setOptions] = useState(longFlatOptions);

	const [isLoading, setIsLoading] = useState(false);
	const timeoutRef = useRef(null);

	const debouncedFilterOptions = useCallback((value) => {
		if (timeoutRef.current != null) {
			clearTimeout(timeoutRef.current);
			timeoutRef.current = null;
		}

		setIsLoading(true);

		timeoutRef.current = setTimeout(() => {
			const searchValue = value.toLocaleLowerCase();
			const filteredOptions = longFlatOptions.filter((o) => (o.searchLabel ?? o.label).toLocaleLowerCase().includes(searchValue));

			setOptions(filteredOptions);
			setIsLoading(false);
			timeoutRef.current = null;
		}, 500);
	}, [setIsLoading, setOptions]);

 return <Select
	label="Choose an option"
	value={currentOption}
	onChange={setValue}
	options={options}
	placeholder="Choose an option"
	isSearchable={true}
	searchInputValue={searchValue}
	onSearchInput={(e) => {
		const { value } = e.target;
		setSearchValue(value);
		debouncedFilterOptions(value);
	}}
	isLoading={isLoading}
 />;
}

Select also supports infinite scrolling (for paginated APIs), and will call the onLoadMore callback when scrolling towards the end of the list. The other props expected for this use case are:

  • shouldLoadMore - a boolean indicating whether the onLoadMore callback should be called at all (e.g. when there is a next page of data). Set to false if there are no more pages/there is no more data to fetch
  • isLoadingMore - whether more data is currently being fetched. While the API call issued via the onLoadMore callback is running, this should be true, and will show a loading spinner at the bottom of the list

Note: It is up to you to merge the results of the paginated API calls into one options array, which you pass into the component. The component itself only issues callbacks when required, but you control the state.

The example below shows a Select that simulates an API with filtering and pagination.

import { Select } from '@customerio/pluma-components/react';
import { useState, useCallback } from 'react';

export default function Example() {
	const [value, setValue] = useState(null);
	const [searchValue, setSearchValue] = useState('');
	const currentOption = longFlatOptions.find((o) => o.value === value);

	const paginationInterval = 10;
	const [filteredOptions, setFilteredOptions] = useState(longFlatOptions);
	const [visibleOptionIndex, setVisibleOptionIndex] = useState(paginationInterval);

	const [isLoading, setIsLoading] = useState(false);
	const timeoutRef = useRef(null);

	const debouncedFilterOptions = useCallback((value) => {
		if (timeoutRef.current != null) {
			clearTimeout(timeoutRef.current);
			timeoutRef.current = null;
		}

		setIsLoading(true);

		timeoutRef.current = setTimeout(() => {
			let filteredOptions = longFlatOptions;

			if (value !== '') {
				const searchValue = value.toLocaleLowerCase();
				filteredOptions = longFlatOptions.filter((o) => (o.searchLabel ?? o.label).toLocaleLowerCase().includes(searchValue));
			}

			setFilteredOptions(filteredOptions);
			setIsLoading(false);
			setVisibleOptionIndex(paginationInterval);
			setShouldLoadMore(filteredOptions.length > paginationInterval);
			timeoutRef.current = null;
		}, 500);
	}, [setIsLoading, setFilteredOptions, setVisibleOptionIndex]);

	const options = filteredOptions.slice(0, visibleOptionIndex);

	const [isLoadingMore, setIsLoadingMore] = useState(false);
	const [shouldLoadMore, setShouldLoadMore] = useState(true);

	const onLoadMore = useCallback(() => {
		if (shouldLoadMore) {
			setIsLoadingMore(true);

			setTimeout(() => {
				const remainingOptionCount = filteredOptions.length - visibleOptionIndex;
				const nextVisibleOptionIndex =
					remainingOptionCount >= paginationInterval
						? visibleOptionIndex + paginationInterval
						: visibleOptionIndex + remainingOptionCount;

				setIsLoadingMore(false);
				setVisibleOptionIndex(nextVisibleOptionIndex);

				if (remainingOptionCount <= paginationInterval) {
					setShouldLoadMore(false);
				}
			}, 1000);
		}
	}, [shouldLoadMore, setIsLoadingMore, filteredOptions.length, visibleOptionIndex]);

 return <Select
	label="Choose an option"
	value={currentOption}
	onChange={setValue}
	options={options}
	placeholder="Choose an option"
	isSearchable={true}
	searchInputValue={searchValue}
	onSearchInput={(e) => {
		const { value } = e.target;
		setSearchValue(value);
		debouncedFilterOptions(value);
	}}
	isLoading={isLoading}
	onLoadMore={onLoadMore}
	shouldLoadMore={!isLoading && shouldLoadMore}
	isLoadingMore={isLoadingMore}
	onCloseComplete={() => {
		setSearchValue('');
		debouncedFilterOptions('');
	}}
 />;
}

PlumaProvider configuration

By default, the Select component enters and exits with a CSS transition animation (with a duration of 125ms).

The value can be overridden globally with the componentConfig prop on the PlumaProvider component:

<PlumaProvider
	componentConfig={{
		PlumaSelect: {
			menuAnimationTransitionDuration: 0,
		}
	}}
>
	{children}
</PlumaProvider>

This can be used, for example, in tests, to set the transition time to 0.

Customizing labels

If a string label is not sufficient, a component can be used to render the label.

In React, the label prop accepts any ReactNode.

import { Select, Icon } from '@customerio/pluma-components/react';
import { useState } from 'react';

const options = [
  { value: 'option_1', label: 'Option 1' },
  { value: 'option_2', label: 'Option 2' },
  { value: 'option_3', label: 'Option 3' },
];

export default function Example() {
  const [value, setValue] = useState(null);

  return <Select
    label={<>Custom label <Icon name="campaigns" isInline={true} size="sm" /></>}
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
    options={options}
  />
}

Customizing descriptions

If a string description is not sufficient, a component can be used to render the description.

In React, the description prop accepts any ReactNode.

import { Select, Link } from '@customerio/pluma-components/react';
import { useState } from 'react';

const options = [
  { value: 'option_1', label: 'Option 1' },
  { value: 'option_2', label: 'Option 2' },
  { value: 'option_3', label: 'Option 3' },
];

export default function Example() {
  const [value, setValue] = useState(null);

  return <Select
    ariaLabel="Custom description input"
    description={<>Custom description, <Link href="https://docs.customer.io" isExternal={true} variant="secondary">see docs for details</Link></>}
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
    options={options}
  />
}

Customizing errors

If a string error is not sufficient, a component can be used to render the error.

In React, the error prop accepts any ReactNode.

import { Select, Link } from '@customerio/pluma-components/react';
import { useState } from 'react';

const options = [
  { value: 'option_1', label: 'Option 1' },
  { value: 'option_2', label: 'Option 2' },
  { value: 'option_3', label: 'Option 3' },
];

export default function Example() {
  const [value, setValue] = useState(null);

  return <Select
    ariaLabel="Custom error input"
    error={<>Custom error, <Link href="https://docs.customer.io" isExternal={true} variant="secondary">see docs for help</Link></>}
    value={value}
    onChange={(value) => {
      setValue(value);
    }}
    options={options}
  />
}

Clearable

PlumaSelect accepts an isClearable prop, which renders a "clear" button when it has a value. When the button is clicked, the onChange callback is called with null.

import { Select } from '@customerio/pluma-components/react';
import { useState } from 'react';

export default function Example() {
	const [value, setValue] = useState('option_1');

	return <Select
		label="Choose an option"
		value={value}
		onChange={(value) => setValue(value)}
		options={[
			{ value: 'option_1', label: 'Option 1' },
			{ value: 'option_2', label: 'Option 2' },
			{ value: 'option_3', label: 'Option 3' },
		]}
		placeholder="Choose an option"
		isClearable={true}
	/>
}

API

Element (or elements) that describe the form field. Also used for error messages, as aria-errormessage isn't supported in all assistive technologies: https://a11ysupport.io/tech/aria/aria-errormessage_attribute

String value that labels the form field.

Element (or elements) that label the form field.

The component to render at the header of the menu (after the search input, if present, but before the options list).

Whether a new option with the given value can be created. This can be used to exclude certain values from showing a "Create new" option.

Default:false

Whether the Select can wrap selected values to multiple lines (instead of truncating overflow). Only applies when isMulti is true.

Default:'Create "{value}"'

The label to show for the "Create new" option. If a function is provided, it will be called with the current search input value.

Whether the select dropdown is open initially (when uncontrolled).

A description for the field, providing additional context or hints.

An error message associated with the form field. Boolean: Ember-only. If the error named block is used, this prop can be set to true to indicate an error is present, which will make the error block render.

The component to render at the footer of the menu.

The component to render at the header of the menu (before the search input, if present).

The component to render for an option's icon.

An optional id to be assigned to the input element. Also used to generate ids for labels and error messages. If one isn't provided, it will be generated.

Whether the select is clearable. When the clear button is clicked, the onChange callback is called with null.

Whether the form field is disabled.

Whether the form field is in an invalid state.

Whether the component should show a loading spinner.

Whether more options are currently being loaded.

Whether multiple options can be selected.

Whether the select dropdown is open. Use this for controlled mode.

Whether the select options should be searchable (via an input inside the options menu).

The label text to show with the form field.

Default:125

The duration (in ms) for the select's menu popover animation.

middlewareOptions  { offset?: Partial<{ mainAxis: number; crossAxis: number; alignmentAxis: number | null; }> | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => Partial<{ mainAxis: number; crossAxis: number; alignmentAxis: number | null; }>) | undefined; autoPlacement?: { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; crossAxis?: boolean | undefined; alignment?: Alignment | null | undefined; autoAlignment?: boolean | undefined; allowedPlacements?: Placement[] | undefined; boundary?: Boundary | undefined; } | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; crossAxis?: boolean | undefined; alignment?: Alignment | null | undefined; autoAlignment?: boolean | undefined; allowedPlacements?: Placement[] | undefined; boundary?: Boundary | undefined; }) | undefined; shift?: { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; mainAxis?: boolean | undefined; crossAxis?: boolean | undefined; limiter?: { fn: (state: MiddlewareState) => Coords; options?: any; } | undefined; boundary?: Boundary | undefined; } | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; mainAxis?: boolean | undefined; crossAxis?: boolean | undefined; limiter?: { fn: (state: MiddlewareState) => Coords; options?: any; } | undefined; boundary?: Boundary | undefined; }) | undefined; hide?: { padding?: Padding | undefined; strategy?: "referenceHidden" | "escaped" | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; boundary?: Boundary | undefined; } | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => { padding?: Padding | undefined; strategy?: "referenceHidden" | "escaped" | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; boundary?: Boundary | undefined; }) | undefined; size?: { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; boundary?: Boundary | undefined; apply?: ((args: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; } & { availableWidth: number; availableHeight: number; }) => Promisable<void>) | undefined; } | ((state: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; }) => { padding?: Padding | undefined; rootBoundary?: RootBoundary | undefined; elementContext?: ElementContext | undefined; altBoundary?: boolean | undefined; boundary?: Boundary | undefined; apply?: ((args: { x: number; y: number; placement: Placement; strategy: Strategy; initialPlacement: Placement; middlewareData: MiddlewareData; rects: ElementRects; platform: Platform; elements: Elements; } & { availableWidth: number; availableHeight: number; }) => Promisable<void>) | undefined; }) | undefined; } | undefined

Additional options to pass into Floating UI middleware

The name attribute is used for HTML forms

Called when the active (currently highlighted, but not necessarily selected) option changes when the dropdown is open.

The callback for when a value is selected. When isMulti is true, the value in the callback will be an array of the selected values.

Called when the dropdown finishes closing (after the close animation finishes).

Called when the "Create new" option is selected. Note: onChange is not called when creating a new option: setting the new value should also happen within this callback.

Callback for when the end of the dropdown is reached and more options should be loaded.

Called when the dropdown is opened.

Callback when the open state should change.

A handler for the input event on the search text field. If provided, the Select component won't handle the filtering itself, and it's up to the developer to filter the options.

The component to render for each option in the menu.

Placeholder text to show inside the field.

Default:'Search'

An aria-label to apply to the search field (as it doesn't render with its own label).

A value for the search input. If provided, combine it with onSearchInput to control filtering of the options.

Default:'Search'

Placeholder text to show inside the search field (if enabled).

Whether to allow browser autofill and password managers. When false (default), adds attributes to disable password managers (1Password, LastPass, Bitwarden, Dashlane). Set to true for fields where autofill is desired (e.g., email, password, address fields).

Whether the generated description id should be included in the input's aria-describedby attribute. Disable this when a label`` tag surrounds the input as well as it's description text, for example in an OptionCard` component.

Whether the generated label id should be included in the input's aria-labelledby attribute. Disable this when the label tag surrounds the input as well as it's label text, for example in an OptionCard component.

Whether the onLoadMore callback should be called when the end of the dropdown is reached. If no more paginated options are available, this should be set to false.

By default, form elements will throw an error if no label or aria-label is provided. Disable this to suppress the error, for example when building a custom form element, and you want to handle labeling yourself.

The size of the input element. medium and small are deprecated, use md and sm instead.

Additional classes to be applied to the description element.

Additional classes to be applied to the error element.

Additional classes to be applied to the field node.

Additional classes to be applied to the footer element.

Ember-only: this is used internally to detect whether a label named block is present, and to ignore empty label props if so.

The initial value inside the search input. Only used in snapshots/tests.

Additional classes to be applied to the input node.

Sets the size attribute on the input element. This controls the visible width of the input in characters.

Whether the component should render with "legacy" styles.

Additional classes to be applied to the label node.

Additional classes to apply on the SelectPopover component (the floating element, not the trigger).

Additional inline styles to apply on the SelectPopover component (the floating element, not the trigger).

The current value. Can be either:

  • the value of the selected option item
  • the entire selected option item - can be used when working with async options, and the currently selected option may not yet be available in the options array When isMulti is true, the value is an array of the above value options.

The component to render for the selected value.

Whether to apply the center-baseline variant.

When the Select is searchable, whether it should be possible to create new options if the search input doesn't exactly match any existing options.

Show tooltip only when label is truncated, for all options and group headers. Can be overridden per-option or per-group.

Whether the menu options should render in a virtualized list.