Combobox Primitives
Headless combobox behavior with minimal local styles.
Remix
React Router
React
Archived
value=none
import { css, type Handle } from 'remix/ui'
import * as combobox from 'remix/ui/combobox/primitives'
export default function Example(handle: Handle) {
let value = 'none'
return () => (
<div
mix={[
demoCss,
combobox.onComboboxChange((event) => {
value = event.value ?? 'none'
void handle.update()
}),
]}
>
<label mix={labelCss} for={`${handle.id}-field`}>
Framework
</label>
<combobox.Context name="framework">
<input
id={`${handle.id}-field`}
mix={[inputCss, combobox.input()]}
placeholder="Search frameworks"
/>
<div mix={[surfaceCss, combobox.popover()]}>
<div mix={[listCss, combobox.list()]}>
{frameworks.map((option) => (
<div
key={option.value}
mix={[optionCss, combobox.option(option)]}
>
{option.label}
</div>
))}
</div>
</div>
<input mix={[hiddenInputCss, combobox.hiddenInput()]} />
</combobox.Context>
<p mix={valueCss}>{`value=${value}`}</p>
</div>
)
}
const frameworks = [
{ label: 'Remix', searchValue: ['remix', 'rmx'], value: 'remix' },
{
label: 'React Router',
searchValue: ['react router', 'router'],
value: 'react-router',
},
{ label: 'React', value: 'react' },
{ disabled: true, label: 'Archived', value: 'archived' },
]
const demoCss = css({
display: 'grid',
gap: '8px',
width: 'min(100%, 18rem)',
})
const labelCss = css({
color: '#101010',
font: '600 12px/16px "Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
letterSpacing: 0,
})
const inputCss = css({
boxSizing: 'border-box',
width: '100%',
height: '32px',
border: '1px solid #cfcfcf',
borderRadius: '6px',
color: '#101010',
font: '500 13px/18px "Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
letterSpacing: 0,
padding: '0 10px',
'&:focus-visible': {
borderColor: '#3573f6',
outline: '2px solid #1A72FF',
},
})
const surfaceCss = css({
boxSizing: 'border-box',
margin: 0,
border: '1px solid #cfcfcf',
borderRadius: '6px',
background: '#ffffff',
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.12)',
padding: '4px',
'&:popover-open': {
display: 'block',
},
})
const listCss = css({
display: 'grid',
gap: '2px',
maxHeight: '12rem',
overflow: 'auto',
outline: 0,
})
const optionCss = css({
borderRadius: '4px',
color: '#101010',
font: '500 13px/18px "Inter Variable", Inter, ui-sans-serif, system-ui, sans-serif',
letterSpacing: 0,
padding: '6px 8px',
'&[data-highlighted="true"]': {
background: '#eeeeee',
},
'&[aria-selected="true"]': {
background: '#101010',
color: '#ffffff',
},
'&[aria-disabled="true"]': {
color: '#9a9a9a',
},
})
const hiddenInputCss = css({
display: 'none',
})
const valueCss = css({
margin: 0,
color: '#666666',
font: '500 12px/16px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
})