static/app/components/core/compactSelect/composite.mdx
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.
<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>
<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 consistencyEach region acts like an independent <CompactSelect>, requiring its own value, onChange, and options.
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>
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>;
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>
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>;
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>
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>;
<CompositeSelect> supports the same sizes as other select components: md (default), sm, and xs.
<CompositeSelect size="md">...</CompositeSelect>
<CompositeSelect size="sm">...</CompositeSelect>
<CompositeSelect size="xs">...</CompositeSelect>
Always provide a custom trigger using the trigger render prop. Use <OverlayTrigger.Button> or other <OverlayTrigger> variants.
<CompositeSelect
trigger={props => (
<OverlayTrigger.Button icon={<IconFilter />} {...props}>
Filters
</OverlayTrigger.Button>
)}
>
</CompositeSelect>
[!IMPORTANT] Always spread the
propsonto the<OverlayTrigger>component to ensure proper behavior and accessibility.
Understanding when to use each component:
// 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},
]}
/>
Use for filter controls that span multiple independent categories:
<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>
Use for grouped settings where each group has independent behavior:
<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>
Combine multiple temporal dimensions:
<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>
<CompositeSelect> follows accessibility best practices and meets WCAG 2.2 AA standards:
The component includes:
Region Labels
<CompositeSelect.Region> requires a label proparia-label if the visual label isn't sufficient<CompositeSelect.Region
label="Status"
aria-label="Filter by issue status"
value={status}
onChange={handleChange}
options={options}
/>
Trigger Labels
// Good: Descriptive trigger
<OverlayTrigger.Button {...props}>
Filters: {selectedCount} active
</OverlayTrigger.Button>
// Good: Clear purpose
<OverlayTrigger.Button {...props}>
Date: {month}/{day}
</OverlayTrigger.Button>
Keyboard Navigation
Multiple Selections
For more information, see the WAI-ARIA Combobox practices.