ComboBox
A text-field that allows the user to select values from a provided items array.
The <ComboBox> component combines a text input with a listbox, allowing users to filter a list of options to items matching a query or adding a new value.
Its purpose is to make interaction with software more intuitive by presenting options in a concise, readable manner instead of requiring users to remember cryptic commands or navigate through complex hierarchies
Appearance
The appearance of a component can be customized using the variant and size props. These props adjust the visual style and dimensions of the component, available values are based on the active theme.
| Property | Type | Description |
|---|---|---|
variant | - | The available variants of this component. |
size | - | The available sizes of this component. |
Usage
Controlled Usage with custom Filter
If you want to listen or act while the user is typing into the ComboBox field, you can switch to controlled mode by adding an onChange handler and setting the value manually.
This is especially helpful if you need to customize the filtering. For example, you may only want to show suggestions when the user has typed at least two characters. Furthermore, you can improve the matching, as shown in the example below. In the demo, the user would not receive a suggestion if they typed "ssp" without the custom filter.
import { useState } from 'react';import { ComboBox, Stack, Text } from '@marigold/components';export default () => { const [currentValue, setCurrentValue] = useState<string | undefined>(); return ( <Stack> <ComboBox value={currentValue} onChange={setCurrentValue} defaultSelectedKey={3} label="Animals" > <ComboBox.Option id="red panda">Red Panda</ComboBox.Option> <ComboBox.Option id="cat">Cat</ComboBox.Option> <ComboBox.Option id="dog">Dog</ComboBox.Option> <ComboBox.Option id="aardvark">Aardvark</ComboBox.Option> <ComboBox.Option id="kangaroo">Kangaroo</ComboBox.Option> </ComboBox> <Text weight="black">currentValue: "{currentValue}"</Text> </Stack> );};With sections
When related options are present, organizing them into sections enhances clarity and usability. Grouping options provides additional context and helps users navigate choices more easily. This approach reduces complexity and allows for additional guidance when needed, ensuring a more intuitive experience.
This can be achieved by wrapping the options in the <ComboBox.Section> component. A header is required for each section, which is set using the header prop. It's important to note that headers are not part of the accessibility tree. As a result, they will not be considered when filtering the option list.
When `textValue` is required
Use the textValue prop when children contain non-text elements (e.g., icons,
badges, or other decorative components) - this ensures search functions
revieve plain text equivalents, as non-text elements can create mismatches
between visual and semantic content.
import { ComboBox, Text } from '@marigold/components';export default () => ( <ComboBox label="Genres" width="fit"> {options.map(item => ( <ComboBox.Section key={item.category} header={item.category}> {item.genres.map(genre => ( <ComboBox.Option key={genre.name} id={genre.id} textValue={genre.name} > <Text slot="label">{genre.name}</Text> <Text slot="description">{genre.description}</Text> </ComboBox.Option> ))} </ComboBox.Section> ))} </ComboBox>);const options = [ { category: 'Pop and Dance', genres: [ { id: 'pop', name: 'Pop', description: 'Catchy, upbeat music with mass appeal', }, { id: 'synth-pop', name: 'Synth-pop', description: 'Synthesizer-driven pop music from the 80s', }, { id: 'electropop', name: 'Electropop', description: 'Electronic pop with heavy digital production', }, { id: 'dance-pop', name: 'Dance-pop', description: 'Upbeat pop music designed for dancing', }, { id: 'teen-pop', name: 'Teen pop', description: 'Youth-oriented pop music with catchy hooks', }, { id: 'disco', name: 'Disco', description: 'Dance-oriented 70s music with orchestral arrangements', }, ], }, { category: 'Rock and Alternative', genres: [ { id: 'rock', name: 'Rock', description: 'Guitar-driven music with strong rhythms', }, { id: 'hard-rock', name: 'Hard rock', description: 'Heavier, more aggressive rock style', }, { id: 'punk-rock', name: 'Punk rock', description: 'Raw, fast-paced music with anti-establishment themes', }, { id: 'alternative-rock', name: 'Alternative rock', description: 'Non-mainstream rock emerging from indie scenes', }, { id: 'indie-rock', name: 'Indie rock', description: 'Independent-label rock with DIY ethos', }, { id: 'grunge', name: 'Grunge', description: 'Raw, distorted sound from the Seattle scene', }, { id: 'psychedelic-rock', name: 'Psychedelic rock', description: 'Mind-altering rock with experimental sounds', }, ], }, { category: 'Hip-Hop and R&B', genres: [ { id: 'hip-hop', name: 'Hip-Hop', description: 'Urban music with rhythmic beats and rhyming speech', }, { id: 'rap', name: 'Rap', description: 'Rhythmic vocal style over beat-driven backing', }, { id: 'trap', name: 'Trap', description: 'Southern hip-hop with heavy bass and hi-hats', }, { id: 'r&b', name: 'R&B', description: 'Rhythm and blues combining soul and pop elements', }, { id: 'neo-soul', name: 'Neo-soul', description: 'Modern soul music with hip-hop influences', }, ], }, { category: 'Electronic and Experimental', genres: [ { id: 'edm', name: 'EDM', description: 'Electronic Dance Music for festival crowds', }, { id: 'house', name: 'House', description: 'Repetitive 4/4 beats with synth basslines', }, { id: 'techno', name: 'Techno', description: 'Minimal electronic music with mechanical rhythms', }, { id: 'dubstep', name: 'Dubstep', description: 'Bass-heavy electronic music with wobble effects', }, { id: 'ambient', name: 'Ambient', description: 'Atmospheric, texture-based electronic soundscapes', }, { id: 'industrial', name: 'Industrial', description: 'Harsh electronic sounds mixed with punk elements', }, ], }, { category: 'Jazz and Blues', genres: [ { id: 'jazz', name: 'Jazz', description: 'Improvisational music with swing and blue notes', }, { id: 'smooth-jazz', name: 'Smooth Jazz', description: 'Polished, radio-friendly jazz style', }, { id: 'bebop', name: 'Bebop', description: 'Complex, fast-tempo jazz improvisation', }, { id: 'blues', name: 'Blues', description: 'Soulful music based on blues scales and patterns', }, { id: 'delta-blues', name: 'Delta Blues', description: 'Acoustic blues from the Mississippi Delta', }, { id: 'chicago-blues', name: 'Chicago Blues', description: 'Electric blues with amplified instruments', }, ], }, { category: 'Classical and Orchestral', genres: [ { id: 'classical', name: 'Classical', description: 'Traditional Western art music', }, { id: 'baroque', name: 'Baroque', description: 'Ornate style from 1600-1750 with harpsichords', }, { id: 'opera', name: 'Opera', description: 'Dramatic stage works combining music and theater', }, { id: 'symphony', name: 'Symphony', description: 'Large-scale orchestral compositions', }, { id: 'chamber-music', name: 'Chamber Music', description: 'Classical music for small ensembles', }, ], }, { category: 'Folk and Country', genres: [ { id: 'folk', name: 'Folk', description: 'Traditional music with acoustic instrumentation', }, { id: 'country', name: 'Country', description: 'Storytelling music with roots in rural America', }, { id: 'bluegrass', name: 'Bluegrass', description: 'Fast-paced acoustic music with banjo and fiddle', }, { id: 'americana', name: 'Americana', description: 'Blend of country, folk, and roots music', }, ], }, { category: 'Latin and World', genres: [ { id: 'reggaeton', name: 'Reggaeton', description: 'Puerto Rican blend of reggae and Latin rhythms', }, { id: 'salsa', name: 'Salsa', description: 'Energetic Cuban-derived dance music', }, { id: 'bossa-nova', name: 'Bossa Nova', description: 'Brazilian jazz-samba fusion with smooth rhythms', }, { id: 'flamenco', name: 'Flamenco', description: 'Passionate Spanish music with guitar and dance', }, { id: 'afrobeats', name: 'Afrobeats', description: 'West African pop music with complex rhythms', }, ], }, { category: 'Metal and Hard Music', genres: [ { id: 'heavy-metal', name: 'Heavy Metal', description: 'Loud, aggressive music with distorted guitars', }, { id: 'thrash-metal', name: 'Thrash Metal', description: 'Fast, technical metal with punk influences', }, { id: 'death-metal', name: 'Death Metal', description: 'Extreme metal with growled vocals and blast beats', }, { id: 'doom-metal', name: 'Doom Metal', description: 'Slow, heavy metal with dark atmospheres', }, ], }, { category: 'Reggae and Caribbean', genres: [ { id: 'reggae', name: 'Reggae', description: 'Jamaican music with offbeat rhythms', }, { id: 'ska', name: 'Ska', description: 'Jamaican precursor to reggae with walking bassline', }, { id: 'dancehall', name: 'Dancehall', description: 'Digital reggae style with rapid lyrical delivery', }, { id: 'soca', name: 'Soca', description: 'Upbeat Caribbean dance music from Trinidad', }, ], },];Working with asynchronous Data
The ComboBox component supports working with asynchronous data. In the example below, the useAsyncList hook is used to handle the loading and filtering of data from the server.
import { ComboBox, useAsyncList } from '@marigold/components';export default () => { const list = useAsyncList<{ name: string }>({ async load({ signal, filterText }) { const res = await fetch( `https://swapi.py4e.com/api/people/?search=${filterText}`, { signal, } ); const json = await res.json(); return { items: json.results, }; }, }); return ( <ComboBox label="Star Wars Character Lookup" value={list.filterText} onChange={list.setFilterText} items={list.items} > {(item: { name: string }) => ( <ComboBox.Option id={item.name}>{item.name}</ComboBox.Option> )} </ComboBox> );};Displaying Suggestions
Opening the suggestion popover can be triggered through various interactions. This behavior can be adjusted by the menuTrigger prop:
input(default): Open when the user edits the input text.focus: Open when the user focuses the<ComboBox>input.manual: Open when the user presses the trigger button or uses the arrow keys.
The below examples will display the suggestions when the input field is focused.
import { ComboBox } from '@marigold/components';export default () => ( <ComboBox label="Animals" menuTrigger="focus"> <ComboBox.Option id="red panda">Red Panda</ComboBox.Option> <ComboBox.Option id="cat">Cat</ComboBox.Option> <ComboBox.Option id="dog">Dog</ComboBox.Option> <ComboBox.Option id="aardvark">Aardvark</ComboBox.Option> <ComboBox.Option id="kangaroo">Kangaroo</ComboBox.Option> <ComboBox.Option id="snake">Snake</ComboBox.Option> <ComboBox.Option id="vegan">Vegan</ComboBox.Option> <ComboBox.Option id="mar">Margrita</ComboBox.Option> </ComboBox>);Item Actions
The <ComboBox.Option> component supports an onAction prop that triggers a callback when the user performs an action on an item. This is useful for triggering side effects such as navigating to a detail view or opening an edit modal.
By combining controlled input (value/onChange) with allowsCustomValue and a conditional <ComboBox.Option>, you can offer a dynamic "Create new" option that reflects what the user has typed. This is a common pattern for comboboxes that allow creating new entries inline.
`onAction` vs `onSelectionChange`
The onAction prop on individual options differs from the onSelectionChange prop on the <ComboBox> itself. Use onSelectionChange when you need to track and manage the selected value. Use onAction when you need to perform a side effect when an item is activated, regardless of selection state. Note that onAction should not replace the primary selection behavior - it is intended for supplementary actions.
import { useState } from 'react';import { ComboBox, Stack, Text } from '@marigold/components';export default () => { const [lastAction, setLastAction] = useState<string | null>(null); const [selectedKey, setSelectedKey] = useState<string | null>(null); return ( <Stack space={4}> <ComboBox label="Projects" menuTrigger="focus" onSelectionChange={key => setSelectedKey(key as string)} > <ComboBox.Section key="actions" header="Actions"> <ComboBox.Option id="new" onAction={() => setLastAction('Create new file...')} > New file </ComboBox.Option> <ComboBox.Option id="open" onAction={() => setLastAction(`Opening details for file ${selectedKey}...`) } > Open </ComboBox.Option> </ComboBox.Section> <ComboBox.Section key="files" header="Files"> <ComboBox.Option id="file-1"> Top Secret - RUI Initative </ComboBox.Option> <ComboBox.Option id="file-2">Tech Radar</ComboBox.Option> <ComboBox.Option id="file-3">Who is Claude?</ComboBox.Option> </ComboBox.Section> </ComboBox> <Text weight="black">Selected:</Text> {selectedKey ?? 'None'} <Text weight="black">Last action:</Text> {lastAction ?? 'None'} </Stack> );};Props
ComboBox
Prop
Type
ComboBox.Option
Prop
Type
ComboBox.Section
Prop
Type