All components
Combobox
Filterable input with a suggestion popup, item indicators, and an empty state slot.
City
12345678910111213141516171819202122232425262728293031import { Combobox } from "#components/ui/base-ui/combobox.tsx";
const cities = ["Amsterdam", "Berlin", "Lisbon", "Oslo", "Porto"];
export function Example() { return ( // variant/size resolve once on Root; every part inherits them via // context — IndicatorGroup and ItemIndicator ship default icons <Combobox.Root items={cities} variant="outline" size="md"> <Combobox.Label>City</Combobox.Label> <Combobox.Control> <Combobox.Input placeholder="Search cities" /> <Combobox.IndicatorGroup /> </Combobox.Control> {/* Positioner portals the popup and positions it under the input */} <Combobox.Positioner> <Combobox.Popup> <Combobox.Empty>No matches</Combobox.Empty> <Combobox.List> {(city) => ( <Combobox.Item key={city} value={city}> <Combobox.ItemText>{city}</Combobox.ItemText> <Combobox.ItemIndicator /> </Combobox.Item> )} </Combobox.List> </Combobox.Popup> </Combobox.Positioner> </Combobox.Root> );}Variants
City
outlineCity
subtleCity
flushedSizes
City
xsCity
smCity
mdCity
lgInstall
1npx stylexblocks add comboboxSource
components/ui/base-ui/combobox.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818import * as stylex from "@stylexjs/stylex";import { useContext } from "react";import { Combobox as BaseCombobox, type ComboboxRootProps,} from "@base-ui-components/react/combobox";import { mergeProps } from "@base-ui-components/react/merge-props";import { createSlotRecipeContext } from "#lib/base-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, resolveSlotStyles, type SlotRecipeVariants, type StyleOverrideProps, type WithStyleOverrides,} from "#lib/recipe-props.ts";import { semanticShadows } from "@stylexblocks/ui/semantic-tokens/shadows.stylex.ts";import { comboboxIndicatorGroupIconVars, comboboxItemIconVars,} from '@stylexblocks/ui/slot-recipes/combobox.stylex.ts'
import { colorPalette } from '@stylexblocks/ui/color-palette.stylex.ts'import { cursor } from '@stylexblocks/ui/tokens/cursor.stylex.ts'import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { radii } from '@stylexblocks/ui/tokens/radii.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { semanticRadii } from '@stylexblocks/ui/semantic-tokens/radii.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const comboboxRoot = stylex.create({ base: { display: 'flex', flexDirection: 'column', gap: spacing._1_5, width: sizes.full, },})
const comboboxRootSizes = stylex.create({ xs: { '--combobox-input-height': sizes.controlXs, '--combobox-input-padding-x': spacing._2, '--combobox-indicator-size': sizes._3_5, }, sm: { '--combobox-input-height': sizes.controlSm, '--combobox-input-padding-x': spacing._2_5, '--combobox-indicator-size': sizes._4, }, md: { '--combobox-input-height': sizes.controlMd, '--combobox-input-padding-x': spacing._3, '--combobox-indicator-size': sizes._4, }, lg: { '--combobox-input-height': sizes.controlLg, '--combobox-input-padding-x': spacing._4, '--combobox-indicator-size': sizes._5, },})
const comboboxLabel = stylex.create({ base: { fontWeight: fontWeights.medium, userSelect: 'none', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, },})
const comboboxInput = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', width: sizes.full, minHeight: 'var(--combobox-input-height)', paddingInline: 'var(--combobox-input-padding-x)', '--input-height': 'var(--combobox-input-height)', borderRadius: semanticRadii.control, outline: 'none', userSelect: 'none', textAlign: 'start', opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, cursor: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'not-allowed', }, '--focus-color': colorPalette.focusRing, '--error-color': semanticColors.borderError, '--focus-ring-color': { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, },})
const comboboxInputSizes = stylex.create({ xs: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, }, sm: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, md: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, lg: { fontSize: fontSizes.md, lineHeight: lineHeights.md, },})
const comboboxInputVariants = stylex.create({ outline: { backgroundColor: semanticColors.bg, borderWidth: '1px', borderColor: { default: semanticColors.border, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, }, subtle: { borderWidth: '1px', borderColor: { default: 'transparent', ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', }, backgroundColor: semanticColors.bgMuted, outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, }, flushed: { backgroundColor: 'transparent', borderBottomWidth: '1px', borderBottomColor: semanticColors.border, borderRadius: 0, paddingInline: 0, borderColor: { default: null, ':is([data-invalid], [aria-invalid="true"])': 'var(--error-color)', ':focus-visible': 'var(--focus-color)', }, boxShadow: { default: null, ':focus-visible': '0px 1px 0px 0px var(--focus-color)', }, },})
const comboboxTrigger = stylex.create({ base: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', pointerEvents: 'auto', '--input-height': 'var(--combobox-input-height)', },})
const comboboxTriggerSizes = stylex.create({ xs: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, gap: spacing._1, }, sm: { fontSize: fontSizes.xs, lineHeight: lineHeights.xs, gap: spacing._1, }, md: { fontSize: fontSizes.sm, lineHeight: lineHeights.sm, gap: spacing._2, }, lg: { fontSize: fontSizes.md, lineHeight: lineHeights.md, paddingBlock: spacing._3, gap: spacing._2, },})
const comboboxClearTrigger = stylex.create({ base: { color: semanticColors.fgMuted, pointerEvents: 'auto', outlineWidth: { default: null, ':focus-visible': 'var(--focus-ring-width, 0)', }, outlineOffset: { default: null, ':focus-visible': '0px', }, outlineStyle: { default: null, ':focus-visible': 'solid', }, outlineColor: { default: null, ':focus-visible': colorPalette.focusRing, }, focusRingWidth: '2px', borderRadius: semanticRadii.control, },})
const comboboxControl = stylex.create({ base: { position: 'relative', },})
const comboboxIndicatorGroup = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'center', gap: spacing._1, position: 'absolute', insetInlineEnd: 0, top: 0, bottom: 0, pointerEvents: 'none', paddingInline: 'var(--combobox-input-padding-x)', [comboboxIndicatorGroupIconVars.width]: 'var(--combobox-indicator-size)', [comboboxIndicatorGroupIconVars.height]: 'var(--combobox-indicator-size)', },})
const comboboxIndicatorGroupVariants = stylex.create({ flushed: { paddingInline: 0, },})
const comboboxIndicatorGroupIcon = stylex.create({ base: { width: comboboxIndicatorGroupIconVars.width, height: comboboxIndicatorGroupIconVars.height, },})
const comboboxContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', '--menu-z-index': zIndices.layer3, zIndex: 'calc(var(--menu-z-index) + var(--layer-index, 0))', outline: 'none', maxHeight: 'var(--available-height)', overflowY: 'auto', animationName: { default: null, ':is([data-open], [data-state="open"])': 'fade-in, slide-from-top', ':is([data-closed], [data-state="closed"])': 'fade-out', }, animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.fast, ':is([data-closed], [data-state="closed"])': durations.faster, }, },})
const comboboxContentSizes = stylex.create({ xs: { '--combobox-item-padding-x': spacing._1, '--combobox-item-padding-y': spacing._0_5, '--combobox-indicator-size': sizes._3_5, borderRadius: semanticRadii.panel, padding: spacing._0_5, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, }, sm: { '--combobox-item-padding-x': spacing._1_5, '--combobox-item-padding-y': spacing._1, '--combobox-indicator-size': sizes._4, borderRadius: semanticRadii.panel, padding: spacing._1, fontSize: fontSizes.xs, lineHeight: lineHeights.xs, }, md: { '--combobox-item-padding-x': spacing._2, '--combobox-item-padding-y': spacing._1_5, '--combobox-indicator-size': sizes._4, borderRadius: semanticRadii.panel, padding: spacing._1, fontSize: fontSizes.sm, lineHeight: lineHeights.sm, }, lg: { '--combobox-item-padding-y': spacing._2, '--combobox-item-padding-x': spacing._3, '--combobox-indicator-size': sizes._5, borderRadius: semanticRadii.panel, padding: spacing._1, fontSize: fontSizes.md, lineHeight: lineHeights.md, },})
const comboboxItem = stylex.create({ base: { position: 'relative', userSelect: 'none', display: 'flex', alignItems: 'center', gap: spacing._2, paddingBlock: 'var(--combobox-item-padding-y)', paddingInline: 'var(--combobox-item-padding-x)', cursor: cursor.option, justifyContent: 'space-between', flex: 1, textAlign: 'start', borderRadius: semanticRadii.control, pointerEvents: { default: null, ':is(:disabled, [disabled], [data-disabled])': 'none', }, opacity: { default: null, ':is(:disabled, [disabled], [data-disabled])': 0.5, }, [comboboxItemIconVars.width]: 'var(--combobox-indicator-size)', [comboboxItemIconVars.height]: 'var(--combobox-indicator-size)', },})
const comboboxItemSizes = stylex.create({ xs: { borderRadius: `max(0px, calc(${semanticRadii.panel} - ${sizes._0_5}))`, }, sm: { borderRadius: `max(0px, calc(${semanticRadii.panel} - ${sizes._0_5}))`, }, md: { borderRadius: `max(0px, calc(${semanticRadii.panel} - ${sizes._1}))`, }, lg: { borderRadius: `max(0px, calc(${semanticRadii.panel} - ${sizes._1}))`, },})
const comboboxItemIcon = stylex.create({ base: { width: comboboxItemIconVars.width, height: comboboxItemIconVars.height, },})
const comboboxEmpty = stylex.create({ base: { paddingBlock: 'var(--combobox-item-padding-y)', paddingInline: 'var(--combobox-item-padding-x)', },})
const comboboxItemText = stylex.create({ base: { flex: 1, },})
const comboboxItemGroup = stylex.create({ base: { paddingBottom: { default: 'var(--combobox-item-padding-y)', ':last-child': 0, }, },})
const comboboxItemGroupLabel = stylex.create({ base: { fontWeight: fontWeights.medium, paddingBlock: 'var(--combobox-item-padding-y)', paddingInline: 'var(--combobox-item-padding-x)', },})
const comboboxItemIndicator = stylex.create({ base: {},})
const comboboxItemIndicatorSizes = stylex.create({ md: { display: 'flex', alignItems: 'center', justifyContent: 'center', },})
export type ComboboxVariant = keyof typeof comboboxInputVariants
export type ComboboxSize = keyof typeof comboboxRootSizes
const comboboxRecipe = defineSlotRecipe({ slots: { root: { styles: comboboxRoot, sizes: comboboxRootSizes }, label: { styles: comboboxLabel }, input: { styles: comboboxInput, sizes: comboboxInputSizes, variants: comboboxInputVariants, }, trigger: { styles: comboboxTrigger, sizes: comboboxTriggerSizes }, clearTrigger: { styles: comboboxClearTrigger }, control: { styles: comboboxControl }, indicatorGroup: { styles: comboboxIndicatorGroup, variants: comboboxIndicatorGroupVariants, }, content: { styles: comboboxContent, sizes: comboboxContentSizes }, item: { styles: comboboxItem, sizes: comboboxItemSizes }, empty: { styles: comboboxEmpty }, itemText: { styles: comboboxItemText }, itemGroup: { styles: comboboxItemGroup }, itemGroupLabel: { styles: comboboxItemGroupLabel }, itemIndicator: { styles: comboboxItemIndicator, sizes: comboboxItemIndicatorSizes }, }, defaultVariants: { size: "md", variant: "outline" },});
// chrome the generated recipe leaves to the composition layer: unstyled// trigger buttons, popup panel surface, and item hover feedbackconst chrome = stylex.create({ btnReset: { appearance: "none", backgroundColor: "transparent", borderWidth: 0, borderStyle: "none", padding: 0, margin: 0, fontFamily: "inherit", fontSize: "inherit", color: "inherit", cursor: "pointer", textAlign: "inherit", }, anchor: { position: "relative" }, panel: { backgroundColor: semanticColors.bgPanel, boxShadow: semanticShadows.overlay, minWidth: "var(--anchor-width)", }, itemHover: { ":hover": { backgroundColor: semanticColors.bgMuted } }, // Empty keeps its wrapper mounted (role=status) and only clears children empty: { ":empty": { display: "none" }, },});
const panelLocal = stylex.create({ fieldTrigger: { // the variant recipes set border width/color but rely on the UA border // style of a native input; a reset button needs it explicit borderStyle: "solid", cursor: "pointer", textAlign: "start", }, fieldValue: { alignItems: "center", display: "inline-flex", minWidth: 0, }, fieldPlaceholder: { color: semanticColors.fgSubtle, }, panel: { display: "flex", flexDirection: "column", overflow: "hidden", padding: 0, }, search: { borderBlockEndColor: semanticColors.border, borderBlockEndStyle: "solid", borderBlockEndWidth: "1px", borderRadius: 0, flexShrink: 0, }, searchInput: { backgroundColor: "transparent", borderRadius: 0, borderStyle: "none", color: "inherit", minHeight: "unset", outlineStyle: "none", paddingBlock: "var(--combobox-item-padding-y)", paddingInline: "var(--combobox-item-padding-x)", width: "100%", }, panelList: { borderRadius: 0, flexGrow: 1, overflowY: "auto", },});
function ChevronDownIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="m4 6 4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
function CheckIcon(props: React.ComponentProps<"svg">) { return ( <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true" {...props}> <path d="m3.5 8.5 3 3 6-7" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" /> </svg> );}
const { withProvider, withContext, VariantContext } = createSlotRecipeContext(comboboxRecipe);
type ComboboxVariants = SlotRecipeVariants<typeof comboboxRecipe>;
function ComboboxRootNode( props: ComboboxRootProps<unknown> & { className?: string; style?: React.CSSProperties },) { const { className, style, children, ...rest } = props; return ( <div className={className} style={style}> <BaseCombobox.Root {...rest}>{children}</BaseCombobox.Root> </div> );}
const StyledRoot = withProvider(ComboboxRootNode, "root", "ComboboxRoot");
/** * Combobox — Base UI's combobox parts wearing the generated slot recipe. * `<Combobox.Root variant size>` renders the recipe's root layout element * around Base UI's context-only Root; every part inherits the variants * through context. Popup portals through `Combobox.Positioner`; * IndicatorGroup and ItemIndicator ship default chevron / check icons. */function Root<Value, Multiple extends boolean | undefined = false>( props: ComboboxRootProps<Value, Multiple> & Partial<ComboboxVariants> & StyleOverrideProps & { className?: string },) { return <StyledRoot {...(props as React.ComponentProps<typeof StyledRoot>)} />;}Root.displayName = "ComboboxRoot";
const StyledIndicatorGroup = withContext("div", "indicatorGroup", "ComboboxIndicatorGroup");
function IndicatorGroup( props: React.ComponentProps<typeof StyledIndicatorGroup>,) { const { children, ...rest } = props; return ( <StyledIndicatorGroup {...rest}> {children ?? <ChevronDownIcon {...stylex.props(comboboxIndicatorGroupIcon.base)} />} </StyledIndicatorGroup> );}IndicatorGroup.displayName = "ComboboxIndicatorGroup";
const StyledItemIndicator = withContext( BaseCombobox.ItemIndicator, (v) => [ ...resolveSlotStyles(comboboxRecipe.slots.itemIndicator, v), comboboxItemIndicatorSizes.md, ], "ComboboxItemIndicator",);
function ItemIndicator( props: React.ComponentProps<typeof BaseCombobox.ItemIndicator> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledItemIndicator {...rest}> {children ?? <CheckIcon {...stylex.props(comboboxItemIcon.base)} />} </StyledItemIndicator> );}ItemIndicator.displayName = "ComboboxItemIndicator";
const StyledIcon = withContext(BaseCombobox.Icon, () => [], "ComboboxIcon");
/** * Icon — trigger chevron for the input-inside-popup pattern, where the * trigger (not the input) is the visible form control. Ships the default * chevron; pass children to replace it. */function Icon( props: React.ComponentProps<typeof BaseCombobox.Icon> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledIcon {...rest}> {children ?? <ChevronDownIcon {...stylex.props(comboboxIndicatorGroupIcon.base)} />} </StyledIcon> );}Icon.displayName = "ComboboxIcon";
function Positioner({ children, ...rest}: React.ComponentProps<typeof BaseCombobox.Positioner>) { return ( <BaseCombobox.Portal> <BaseCombobox.Positioner sideOffset={4} {...rest}> {children} </BaseCombobox.Positioner> </BaseCombobox.Portal> );}
const StyledFieldTrigger = withContext( BaseCombobox.Trigger, (v) => [ chrome.btnReset, ...resolveSlotStyles(comboboxRecipe.slots.input, v), panelLocal.fieldTrigger, ], "ComboboxFieldTrigger",);
/** * FieldTrigger — the select-look trigger for the popup layout: wears the * input recipe as a button and ships the default chevron. Pair with * `Combobox.Value` for the selected label. */function FieldTrigger( props: React.ComponentProps<typeof BaseCombobox.Trigger> & StyleOverrideProps,) { const { children, ...rest } = props; return ( <StyledFieldTrigger {...rest}> {children} <ChevronDownIcon {...stylex.props(comboboxIndicatorGroupIcon.base)} /> </StyledFieldTrigger> );}FieldTrigger.displayName = "ComboboxFieldTrigger";
const FieldValueBox = withContext("span", () => [panelLocal.fieldValue], "ComboboxFieldValueBox");
/** * FieldValue — the trigger's value slot: wraps `Combobox.Value` with the * selection layout (icon + label compositions stay on one line) and falls * back to a muted `placeholder` while nothing is selected. Children follow * Value's contract — a node, or a render function receiving the selection. */function FieldValue( props: React.ComponentProps<typeof BaseCombobox.Value> & StyleOverrideProps & { placeholder?: React.ReactNode },) { const { placeholder, children, palette, style, className, ...rest } = props; const fallback = ( <span {...stylex.props(panelLocal.fieldPlaceholder)}>{placeholder}</span> ); return ( <FieldValueBox palette={palette} style={style} className={className}> <BaseCombobox.Value {...rest}> {typeof children === "function" ? (value: unknown) => children(value) ?? fallback : (children ?? fallback)} </BaseCombobox.Value> </FieldValueBox> );}FieldValue.displayName = "ComboboxFieldValue";
/** * Search — the popup's input row. The row mirrors the content recipe's * padding and the input adopts the item metrics, so the input's text column * lines up exactly with the item text below it. */function Search( props: WithStyleOverrides<React.ComponentProps<typeof BaseCombobox.Input>> & { className?: string; },) { const { palette, style, className, ...rest } = props; const v = useContext(VariantContext); return ( <div {...stylex.props(comboboxContentSizes[v.size], panelLocal.search, style)}> <BaseCombobox.Input {...mergeProps(rest, stylex.props(palette, panelLocal.searchInput), { className })} /> </div> );}Search.displayName = "ComboboxSearch";
export const Combobox = { Root, Label: withContext("span", "label", "ComboboxLabel"), Control: withContext( "div", (v) => [...resolveSlotStyles(comboboxRecipe.slots.control, v), chrome.anchor], "ComboboxControl", ), Input: withContext(BaseCombobox.Input, "input", "ComboboxInput"), Trigger: withContext( BaseCombobox.Trigger, (v) => [chrome.btnReset, ...resolveSlotStyles(comboboxRecipe.slots.trigger, v)], "ComboboxTrigger", ), Clear: withContext( BaseCombobox.Clear, (v) => [chrome.btnReset, ...resolveSlotStyles(comboboxRecipe.slots.clearTrigger, v)], "ComboboxClear", ), IndicatorGroup, // renders the selected item's label (or a render-prop fallback) on the // trigger in the input-inside-popup pattern Value: BaseCombobox.Value, Icon, Positioner, Popup: withContext( BaseCombobox.Popup, (v) => [...resolveSlotStyles(comboboxRecipe.slots.content, v), chrome.panel], "ComboboxPopup", ), List: BaseCombobox.List, Item: withContext( BaseCombobox.Item, (v) => [...resolveSlotStyles(comboboxRecipe.slots.item, v), chrome.itemHover], "ComboboxItem", ), ItemText: withContext("span", "itemText", "ComboboxItemText"), ItemIndicator, Empty: withContext( BaseCombobox.Empty, (v) => [...resolveSlotStyles(comboboxRecipe.slots.empty, v), chrome.empty], "ComboboxEmpty", ), Group: withContext(BaseCombobox.Group, "itemGroup", "ComboboxGroup"), GroupLabel: withContext(BaseCombobox.GroupLabel, "itemGroupLabel", "ComboboxGroupLabel"), // input-inside-popup layout FieldTrigger, FieldValue, Panel: withContext( BaseCombobox.Popup, (v) => [ ...resolveSlotStyles(comboboxRecipe.slots.content, v), chrome.panel, panelLocal.panel, ], "ComboboxPanel", ), Search, PanelList: withContext( "div", (v) => [comboboxContentSizes[v.size], panelLocal.panelList], "ComboboxPanelList", ),};
Examples
Installable blocks built with Combobox — real use cases, delivered as source you own.
Country
Country Picker
Searchable country picker with flags, Intl-localized names, and ISO 3166-1 values — built on the accessible combobox pattern.
Model Picker
Settings menu for choosing an inference model — Fast switch, Effort submenu, and a searchable combobox for the model list.
Timezone
Timezone Picker
Select-style timezone picker with a search input inside the panel — IANA zones and live offsets from Intl, filtered accessibly.