Back to Sentry

CompactSelect

static/app/components/core/compactSelect/compactSelect.mdx

26.5.013.0 KB
Original Source

import {useState} from 'react';

import {CompactSelect} from '@sentry/scraps/compactSelect'; import {OverlayTrigger} from '@sentry/scraps/overlayTrigger';

import * as Storybook from 'sentry/stories';

export const documentation = import('!!type-loader!@sentry/scraps/compactSelect');

<CompactSelect> is a general-purpose dropdown select component designed for use outside of forms. It's highly capable, supporting features like sections, search, multi-select, custom triggers, virtualization, and more.

Use <CompactSelect> for UI controls like project selectors, environment filters, and toolbar dropdowns. For form fields, use <Select> or <SelectControl> instead.

jsx
<CompactSelect
  value={value}
  onChange={option => setValue(option.value)}
  options={[
    {value: '1', label: 'Option 1'},
    {value: '2', label: 'Option 2'},
    {value: '3', label: 'Option 3'},
  ]}
/>

Basic Usage

In the simplest case, provide a value, onChange handler, and an array of options. The component is controlled—it doesn't maintain its own selection state.

export function SimpleDemo() { const [value, setValue] = useState(''); const options = [ {value: '', label: 'All'}, {value: '2', label: '2XX', details: 'Success responses'}, {value: '3', label: '3XX', details: 'Redirects'}, {value: '4', label: '4XX', details: 'Client errors'}, {value: '5', label: '5XX', details: 'Server errors'}, ]; return ( <CompactSelect value={value} onChange={newValue => setValue(newValue.value)} options={options} /> ); }

<Storybook.Demo minHeight="400px" align="start"> <SimpleDemo /> </Storybook.Demo>

jsx
const [value, setValue] = useState('');
const options = [
  {value: '', label: 'All'},
  {value: '2', label: '2XX', details: 'Success responses'},
  {value: '3', label: '3XX', details: 'Redirects'},
];

<CompactSelect
  value={value}
  onChange={option => setValue(option.value)}
  options={options}
/>;

[!NOTE] Options can include an optional details property to provide additional context shown in the dropdown.

Sections

Group related options into sections using nested option objects. Each section has a key, label, and an array of options.

export function SectionsDemo() { const [values, setValues] = useState(['Woody', 'WALL-E']); const options = [ { key: 'toy-story', label: 'Toy Story', options: [ {value: 'Woody', label: 'Woody'}, {value: 'Buzz', label: 'Buzz Lightyear'}, {value: 'Rex', label: 'Rex'}, ], }, { key: 'wall-e', label: 'WALL-E', options: [ {value: 'WALL-E', label: 'WALL-E'}, {value: 'EVE', label: 'EVE'}, {value: 'M-O', label: 'M-O'}, ], }, ]; return ( <CompactSelect multiple value={values} onChange={selected => setValues(selected.map(opt => opt.value))} options={options} trigger={props => <OverlayTrigger.Button {...props} prefix="Character" />} /> ); }

<Storybook.Demo minHeight="400px" align="start"> <SectionsDemo /> </Storybook.Demo>

jsx
<CompactSelect
  value={value}
  onChange={handleChange}
  options={[
    {
      key: 'section1',
      label: 'Section 1',
      options: [
        {value: 'a', label: 'Option A'},
        {value: 'b', label: 'Option B'},
      ],
    },
    {
      key: 'section2',
      label: 'Section 2',
      options: [
        {value: 'c', label: 'Option C'},
        {value: 'd', label: 'Option D'},
      ],
    },
  ]}
/>

Sizes

<CompactSelect> supports three sizes: md (default), sm, and xs.

export function SizesDemo() { const [value, setValue] = useState('1'); const options = [ {value: '1', label: 'Option 1'}, {value: '2', label: 'Option 2'}, ]; return ( <Storybook.Grid> <CompactSelect size="md" value={value} onChange={opt => setValue(opt.value)} trigger={props => <OverlayTrigger.Button {...props} prefix="Size" />} options={options} /> <CompactSelect size="sm" value={value} onChange={opt => setValue(opt.value)} trigger={props => <OverlayTrigger.Button {...props} prefix="Size" />} options={options} /> <CompactSelect size="xs" value={value} onChange={opt => setValue(opt.value)} trigger={props => <OverlayTrigger.Button {...props} prefix="Size" />} options={options} /> </Storybook.Grid> ); }

<Storybook.Demo minHeight="400px" align="start"> <SizesDemo /> </Storybook.Demo>

jsx
<CompactSelect size="md" options={options} />
<CompactSelect size="sm" options={options} />
<CompactSelect size="xs" options={options} />

Multiple Selection

Set multiple to enable multi-select mode. The value prop should be an array, and onChange receives an array of selected options.

export function MultiSelectDemo() { const [values, setValues] = useState(['2', '4']); const options = [ {value: '1', label: '1XX'}, {value: '2', label: '2XX'}, {value: '3', label: '3XX'}, {value: '4', label: '4XX'}, {value: '5', label: '5XX'}, ]; return ( <CompactSelect multiple value={values} onChange={selected => setValues(selected.map(opt => opt.value))} options={options} trigger={props => <OverlayTrigger.Button {...props} prefix="Status" />} /> ); }

<Storybook.Demo minHeight="400px" align="start"> <MultiSelectDemo /> </Storybook.Demo>

jsx
const [values, setValues] = useState(['2', '4']);

<CompactSelect
  multiple
  value={values}
  onChange={selected => setValues(selected.map(opt => opt.value))}
  options={options}
/>;

Searchable

Enable search to allow users to filter options by typing. Essential for lists with many options. Pass true for default behavior, or a SearchConfig object to customize placeholder, filtering, or the onChange callback.

export function SearchDemo() { const [value, setValue] = useState('US'); return ( <CompactSelect search value={value} onChange={opt => setValue(opt.value)} trigger={props => <OverlayTrigger.Button {...props} prefix="Country" />} options={[ {value: 'US', label: 'United States'}, {value: 'CA', label: 'Canada'}, {value: 'MX', label: 'Mexico'}, {value: 'UK', label: 'United Kingdom'}, {value: 'FR', label: 'France'}, {value: 'DE', label: 'Germany'}, ]} /> ); }

<Storybook.Demo minHeight="400px" align="start"> <SearchDemo /> </Storybook.Demo>

jsx
<CompactSelect search options={options} />

Clearable

Set clearable to add a clear button that resets the selection.

export function ClearableDemo() { const [value, setValue] = useState('2'); const options = [ {value: '1', label: 'Option 1'}, {value: '2', label: 'Option 2'}, {value: '3', label: 'Option 3'}, ]; return ( <CompactSelect clearable value={value} onChange={option => setValue(option?.value || '')} options={options} /> ); }

<Storybook.Demo minHeight="400px" align="start"> <ClearableDemo /> </Storybook.Demo>

jsx
<CompactSelect
  clearable
  value={value}
  onChange={option => setValue(option?.value || '')}
  options={options}
/>

Custom Trigger

<CompactSelect> should always be triggered by an <OverlayTrigger>. By default, it will render an <OverlayTrigger.Button> for you. You can pass a custom trigger with the trigger prop.

Note that props passed to the trigger need to be spread onto the underlying <OverlayTrigger>. Always use an <OverlayTrigger>, there will be type errors when you're trying to use other components.

<OverlayTrigger> will inherit props like size, isOpen and disabled from the <CompactSelect>, so you don't need to pass them manually.

jsx
<CompactSelect
  options={options}
  trigger={props => (
    <OverlayTrigger.Button {...props} prefix="Status Code" icon={<IconSiren />}>
      {value ? `Status: ${value}` : 'Choose status'}
    </OverlayTrigger.Button>
  )}
/>

[!IMPORTANT] Always spread the props onto the <OverlayTrigger> component. This ensures proper accessibility and behavior.

Combined Features

Combine multiple features for powerful selection interfaces:

export function CombinedDemo() { const [values, setValues] = useState(['prod']); const options = [ { key: 'production', label: 'Production', options: [ {value: 'prod', label: 'Production'}, {value: 'prod-eu', label: 'Production EU'}, ], }, { key: 'staging', label: 'Staging', options: [ {value: 'staging', label: 'Staging'}, {value: 'staging-eu', label: 'Staging EU'}, ], }, { key: 'development', label: 'Development', options: [ {value: 'dev', label: 'Development'}, {value: 'local', label: 'Local'}, ], }, ]; return ( <Storybook.Grid> <CompactSelect size="md" multiple search clearable trigger={props => <OverlayTrigger.Button {...props} prefix="Environment" />} value={values} onChange={selected => setValues(selected.map(opt => opt.value))} options={options} /> <CompactSelect size="sm" multiple search clearable trigger={props => <OverlayTrigger.Button {...props} prefix="Environment" />} value={values} onChange={selected => setValues(selected.map(opt => opt.value))} options={options} /> <CompactSelect size="xs" multiple search clearable trigger={props => <OverlayTrigger.Button {...props} prefix="Environment" />} value={values} onChange={selected => setValues(selected.map(opt => opt.value))} options={options} /> </Storybook.Grid> ); }

<Storybook.Demo minHeight="480px" align="start"> <CombinedDemo /> </Storybook.Demo>

jsx
<CompactSelect
  multiple
  search
  clearable
  trigger={props => <OverlayTrigger.Button {...props} prefix="Environment" />}
  value={values}
  onChange={handleChange}
  options={sectionsWithOptions}
/>

Performance Optimizations

Virtualization

For large lists (hundreds of items), <CompactSelect> automatically virtualizes the option list, rendering only visible items. This is enabled by default and requires no configuration.

Options Caching

For expensive option computations or API-fetched data, use the useCompactSelectOptionsCache hook to memoize options:

jsx
import {useCompactSelectOptionsCache} from 'sentry/views/insights/common/utils/useCompactSelectOptionsCache';

function MyComponent() {
  const projects = useProjects(); // Expensive fetch

  const {options: projectOptions} = useCompactSelectOptionsCache(
    projects.map(p => ({
      value: p.id,
      label: p.name,
    }))
  );

  return (
    <CompactSelect
      search
      options={projectOptions}
      value={selectedProject}
      onChange={handleChange}
    />
  );
}

This hook prevents unnecessary re-renders and option recomputations when the parent component re-renders.

Usage Patterns

Project Selectors

jsx
<CompactSelect
  search
  multiple
  clearable
  trigger={props => <OverlayTrigger.Button {...props} prefix="Projects" />}
  value={selectedProjects}
  onChange={handleProjectChange}
  options={projectOptions}
/>

Environment Filters

jsx
<CompactSelect
  trigger={props => (
    <OverlayTrigger.Button {...props} prefix="Env" icon={<IconEnvironment />} />
  )}
  value={environment}
  onChange={option => setEnvironment(option.value)}
  options={environmentOptions}
/>

Grouped Filters

jsx
<CompactSelect
  search
  clearable
  trigger={props => <OverlayTrigger.Button {...props} prefix="Filter" />}
  options={[
    {
      key: 'status',
      label: 'Status',
      options: statusOptions,
    },
    {
      key: 'priority',
      label: 'Priority',
      options: priorityOptions,
    },
  ]}
/>

Accessibility

<CompactSelect> follows the WAI-ARIA Combobox pattern and meets WCAG 2.2 AA standards:

The component includes:

  • role="combobox" and proper ARIA attributes
  • Keyboard navigation (Arrow keys, Enter, Escape)
  • Screen reader announcements for selections
  • Focus management

Developer Responsibilities

Labels

  • Provide context through the prefix prop on <OverlayTrigger.Button> or external labels
  • Use aria-label when the trigger doesn't have visible text

Keyboard Shortcuts

  • Arrow Up/Down: Navigate options
  • Enter: Select option
  • Escape: Close dropdown
  • Type to search: When searchable is enabled

Multiple Selection

  • Clearly communicate how many items are selected
  • Consider using the prefix prop on <OverlayTrigger.Button> to show selection count

For more information, see the WAI-ARIA Combobox practices.