Back to Sentry

CompositeSelect

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

26.4.213.2 KB
Original Source

import {useState} from 'react';

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

import {IconSentry} from 'sentry/icons'; import * as Storybook from 'sentry/stories';

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

<CompositeSelect> is a specialized dropdown component that allows you to combine multiple independent select sections, each with its own single or multi-select behavior. This is useful when users need to make selections across different categories within a single dropdown.

Use <CompositeSelect> when you need a dropdown with multiple independent selection groups, such as filter controls with different criteria or settings with grouped options. For simpler dropdowns, use <CompactSelect> instead.

jsx
<CompositeSelect
  trigger={props => (
    <OverlayTrigger.Button {...props}>Select Options</OverlayTrigger.Button>
  )}
>
  <CompositeSelect.Region
    label="Category 1"
    value={value1}
    onChange={handleChange1}
    options={options1}
  />
  <CompositeSelect.Region
    label="Category 2"
    value={value2}
    onChange={handleChange2}
    options={options2}
  />
</CompositeSelect>

Compound Components

<CompositeSelect> uses a compound component pattern:

  • <CompositeSelect>: The wrapper component that manages the dropdown
  • <CompositeSelect.Region>: Individual selection sections within the dropdown
  • <CompositeSelect.ClearButton>: A "Clear" button for use in menuHeaderTrailingItems that calls your onClick to reset all regions. Re-exported from <CompactSelect>'s internal clear button for visual consistency

Each region acts like an independent <CompactSelect>, requiring its own value, onChange, and options.

Basic Usage

Build a <CompositeSelect> by adding multiple <CompositeSelect.Region> children. Each region is an independent select control.

export function BasicDemo() { const [month, setMonth] = useState('jan'); const [day, setDay] = useState('1'); const monthOptions = [ {value: 'jan', label: 'January'}, {value: 'feb', label: 'February'}, {value: 'mar', label: 'March'}, ]; const dayOptions = [ {value: '1', label: '1'}, {value: '2', label: '2'}, {value: '3', label: '3'}, ]; return ( <CompositeSelect size="sm" trigger={props => ( <OverlayTrigger.Button icon={<IconSentry />} {...props}> Select Date </OverlayTrigger.Button> )} > <CompositeSelect.Region label="Month" value={month} onChange={selection => setMonth(selection.value)} options={monthOptions} /> <CompositeSelect.Region label="Day" value={day} onChange={selection => setDay(selection.value)} options={dayOptions} /> </CompositeSelect> ); }

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

jsx
const [month, setMonth] = useState('jan');
const [day, setDay] = useState('1');

<CompositeSelect
  trigger={props => <OverlayTrigger.Button {...props}>Select Date</OverlayTrigger.Button>}
>
  <CompositeSelect.Region
    label="Month"
    value={month}
    onChange={selection => setMonth(selection.value)}
    options={monthOptions}
  />
  <CompositeSelect.Region
    label="Day"
    value={day}
    onChange={selection => setDay(selection.value)}
    options={dayOptions}
  />
</CompositeSelect>;

Clear Button

Use <CompositeSelect.ClearButton> in menuHeaderTrailingItems to add a "Clear" button to the menu header. When clicked, it calls your onClick handler where you reset each region's value. The menu stays open, matching the behavior of the built-in clear button in <CompactSelect>. The component is the same styled button used internally by <CompactSelect>, ensuring visual consistency.

Only render the button when there is an active selection to clear.

export function ClearButtonDemo() { const monthOptions = [ {value: 'jan', label: 'January'}, {value: 'feb', label: 'February'}, {value: 'mar', label: 'March'}, ]; const tagOptions = [ {value: 'cool', label: 'cool'}, {value: 'funny', label: 'funny'}, {value: 'awesome', label: 'awesome'}, ]; const [month, setMonth] = useState(''); const [tags, setTags] = useState(tagOptions.slice(0, 0).map(o => o.value)); const hasSelection = month !== '' || tags.length > 0; return ( <CompositeSelect size="sm" menuTitle="Filters" menuHeaderTrailingItems={ hasSelection ? ( <CompositeSelect.ClearButton onClick={() => { setMonth(''); setTags([]); }} /> ) : null } trigger={props => ( <OverlayTrigger.Button icon={<IconSentry />} {...props}> Filters </OverlayTrigger.Button> )} > <CompositeSelect.Region label="Month" value={month} onChange={selection => setMonth(selection.value)} options={monthOptions} /> <CompositeSelect.Region label="Tags" multiple value={tags} onChange={selection => setTags(selection.map(s => s.value))} options={tagOptions} /> </CompositeSelect> ); }

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

jsx
const [month, setMonth] = useState(null);
const [tags, setTags] = useState([]);
const hasSelection = month !== null || tags.length > 0;

<CompositeSelect
  menuTitle="Filters"
  menuHeaderTrailingItems={
    hasSelection ? (
      <CompositeSelect.ClearButton
        onClick={() => {
          setMonth(null);
          setTags([]);
        }}
      />
    ) : null
  }
  trigger={props => <OverlayTrigger.Button {...props}>Filters</OverlayTrigger.Button>}
>
  <CompositeSelect.Region
    label="Month"
    value={month}
    onChange={selection => setMonth(selection.value)}
    options={monthOptions}
  />
  <CompositeSelect.Region
    label="Tags"
    multiple
    value={tags}
    onChange={selection => setTags(selection.map(s => s.value))}
    options={tagOptions}
  />
</CompositeSelect>;

Multi-Select Regions

Individual regions can enable multi-select by setting the multiple prop. This allows mixing single and multi-select behavior within the same dropdown.

export function MultiSelectDemo() { const [month, setMonth] = useState('jan'); const [tags, setTags] = useState(['cool', 'awesome']); const monthOptions = [ {value: 'jan', label: 'January'}, {value: 'feb', label: 'February'}, ]; const tagOptions = [ {value: 'cool', label: 'cool'}, {value: 'funny', label: 'funny'}, {value: 'awesome', label: 'awesome'}, ]; return ( <CompositeSelect size="sm" trigger={props => ( <OverlayTrigger.Button icon={<IconSentry />} {...props}> Configure </OverlayTrigger.Button> )} > <CompositeSelect.Region label="Month" value={month} onChange={selection => setMonth(selection.value)} options={monthOptions} /> <CompositeSelect.Region label="Tags" multiple value={tags} onChange={selection => setTags(selection.map(s => s.value))} options={tagOptions} /> </CompositeSelect> ); }

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

jsx
const [month, setMonth] = useState('jan');
const [tags, setTags] = useState(['cool', 'awesome']);

<CompositeSelect
  trigger={props => <OverlayTrigger.Button {...props}>Configure</OverlayTrigger.Button>}
>
  <CompositeSelect.Region
    label="Month"
    value={month}
    onChange={selection => setMonth(selection.value)}
    options={monthOptions}
  />
  <CompositeSelect.Region
    label="Tags"
    multiple
    value={tags}
    onChange={selection => setTags(selection.map(s => s.value))}
    options={tagOptions}
  />
</CompositeSelect>;

Sizes

<CompositeSelect> supports the same sizes as other select components: md (default), sm, and xs.

jsx
<CompositeSelect size="md">...</CompositeSelect>
<CompositeSelect size="sm">...</CompositeSelect>
<CompositeSelect size="xs">...</CompositeSelect>

Custom Trigger

Always provide a custom trigger using the trigger render prop. Use <OverlayTrigger.Button> or other <OverlayTrigger> variants.

jsx
<CompositeSelect
  trigger={props => (
    <OverlayTrigger.Button icon={<IconFilter />} {...props}>
      Filters
    </OverlayTrigger.Button>
  )}
>
</CompositeSelect>

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

CompositeSelect vs CompactSelect

Understanding when to use each component:

Use CompositeSelect when:

  • You need multiple independent selection sections in one dropdown
  • Each section requires different selection behavior (some single-select, some multi-select)
  • Sections represent different categories or dimensions (e.g., date picker with month + day, or filters with status + priority)

Use CompactSelect when:

  • You need a single list of options
  • All selections follow the same behavior (all single-select OR all multi-select)
  • Options can be grouped into sections but represent the same category

Example Comparison

jsx
// CompositeSelect: Independent sections with different behaviors
<CompositeSelect>
  <CompositeSelect.Region label="Main Course" value={main} onChange={setMain} options={mainOptions} />
  <CompositeSelect.Region label="Side" value={side} onChange={setSide} options={sideOptions} />
</CompositeSelect>

// CompactSelect: Single selection from grouped options
<CompactSelect
  value={drink}
  onChange={setDrink}
  options={[
    {key: 'hot', label: 'Hot Drinks', options: hotOptions},
    {key: 'cold', label: 'Cold Drinks', options: coldOptions},
  ]}
/>

Usage Patterns

Multi-Dimensional Filters

Use for filter controls that span multiple independent categories:

jsx
<CompositeSelect
  trigger={props => (
    <OverlayTrigger.Button icon={<IconFilter />} {...props}>
      Filters
    </OverlayTrigger.Button>
  )}
>
  <CompositeSelect.Region
    label="Status"
    multiple
    value={statuses}
    onChange={selection => setStatuses(selection.map(s => s.value))}
    options={statusOptions}
  />
  <CompositeSelect.Region
    label="Priority"
    value={priority}
    onChange={selection => setPriority(selection.value)}
    options={priorityOptions}
  />
  <CompositeSelect.Region
    label="Assignee"
    multiple
    value={assignees}
    onChange={selection => setAssignees(selection.map(s => s.value))}
    options={assigneeOptions}
  />
</CompositeSelect>

Settings with Groups

Use for grouped settings where each group has independent behavior:

jsx
<CompositeSelect
  trigger={props => <OverlayTrigger.Button {...props}>Settings</OverlayTrigger.Button>}
>
  <CompositeSelect.Region
    label="Theme"
    value={theme}
    onChange={selection => setTheme(selection.value)}
    options={themeOptions}
  />
  <CompositeSelect.Region
    label="Notifications"
    multiple
    value={notifications}
    onChange={selection => setNotifications(selection.map(s => s.value))}
    options={notificationOptions}
  />
</CompositeSelect>

Date/Time Pickers

Combine multiple temporal dimensions:

jsx
<CompositeSelect>
  <CompositeSelect.Region
    label="Month"
    value={month}
    onChange={setMonth}
    options={months}
  />
  <CompositeSelect.Region label="Day" value={day} onChange={setDay} options={days} />
  <CompositeSelect.Region label="Year" value={year} onChange={setYear} options={years} />
</CompositeSelect>

Accessibility

<CompositeSelect> follows accessibility best practices and meets WCAG 2.2 AA standards:

The component includes:

  • Proper ARIA attributes for each region
  • Keyboard navigation between regions and options
  • Screen reader announcements for selections

Developer Responsibilities

Region Labels

  • Each <CompositeSelect.Region> requires a label prop
  • Labels clearly identify what each section controls
  • Use aria-label if the visual label isn't sufficient
jsx
<CompositeSelect.Region
  label="Status"
  aria-label="Filter by issue status"
  value={status}
  onChange={handleChange}
  options={options}
/>

Trigger Labels

  • The trigger button should clearly describe the composite selection
  • Consider showing selected values or counts in the trigger text
jsx
// Good: Descriptive trigger
<OverlayTrigger.Button {...props}>
  Filters: {selectedCount} active
</OverlayTrigger.Button>

// Good: Clear purpose
<OverlayTrigger.Button {...props}>
  Date: {month}/{day}
</OverlayTrigger.Button>

Keyboard Navigation

  • Tab: Move between regions
  • Arrow Up/Down: Navigate options within a region
  • Space/Enter: Toggle selection
  • Escape: Close dropdown

Multiple Selections

  • Clearly communicate how many items are selected in each region
  • Consider visual indicators (checkmarks) for multi-select regions

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