Combobox
Basic
package combobox
import "github.com/gsxhq/gsxui/ui"
var frameworks = []struct{ Value, Label string }{
{"next.js", "Next.js"},
{"sveltekit", "SvelteKit"},
{"nuxt.js", "Nuxt.js"},
{"remix", "Remix"},
{"astro", "Astro"},
}
// Basic mirrors shadcn's own combobox-basic.tsx: a five-item single-select
// with no groups and no clear button. Typing filters via ui/combobox.js's
// contains() matcher (hide/show only, no reordering — see its own header);
// name="framework" renders the hidden sr-only <input> form bridge.
component Basic() {
<ui.Combobox name="framework" value="">
<ui.ComboboxInput placeholder="Search framework..." showTrigger class="w-[220px]"/>
<ui.ComboboxContent>
<ui.ComboboxList>
<ui.ComboboxEmpty>No framework found.</ui.ComboboxEmpty>
{ for _, f := range frameworks {
<ui.ComboboxItem value={f.Value} selected={false}>{ f.Label }</ui.ComboboxItem>
} }
</ui.ComboboxList>
</ui.ComboboxContent>
</ui.Combobox>
}
Groups
package combobox
import "github.com/gsxhq/gsxui/ui"
var fruitGroups = []struct {
Label string
Items []struct{ Value, Label string }
}{
{"Citrus", []struct{ Value, Label string }{
{"orange", "Orange"},
{"lemon", "Lemon"},
{"lime", "Lime"},
}},
{"Berries", []struct{ Value, Label string }{
{"strawberry", "Strawberry"},
{"blueberry", "Blueberry"},
{"raspberry", "Raspberry"},
}},
}
// Groups mirrors shadcn's own combobox-groups.tsx: nested groups, each with
// a ComboboxLabel heading, separated by a ComboboxSeparator between groups
// (not after the last one) — the same shape ## select's own scrollable
// example uses for SelectGroup/SelectSeparator.
component Groups() {
<ui.Combobox name="fruit" value="">
<ui.ComboboxInput placeholder="Search fruit..." showTrigger class="w-[220px]"/>
<ui.ComboboxContent>
<ui.ComboboxList>
<ui.ComboboxEmpty>No fruit found.</ui.ComboboxEmpty>
{ for i, g := range fruitGroups {
<ui.ComboboxGroup>
<ui.ComboboxLabel>{ g.Label }</ui.ComboboxLabel>
{ for _, f := range g.Items {
<ui.ComboboxItem value={f.Value} selected={false}>{ f.Label }</ui.ComboboxItem>
} }
</ui.ComboboxGroup>
{ if i < len(fruitGroups)-1 {
<ui.ComboboxSeparator/>
} }
} }
</ui.ComboboxList>
</ui.ComboboxContent>
</ui.Combobox>
}
Clear
package combobox
import "github.com/gsxhq/gsxui/ui"
// Clear mirrors shadcn's own combobox-clear.tsx: showClear renders the X
// button instead of the chevron trigger (ComboboxInput's own
// input group's clear-button descendant rule hides the trigger whenever a
// clear button is present), and a pre-selected value
// server-renders the checked item.
//
// FIX (review round 2, IMPORTANT): a server-selected value must REFLECT in
// the DOM on first paint (docs/superpowers/specs/2026-07-24-tier4-batch-a-
// design.md §4) — combobox.js's init() only SEEDS the input's displayed
// label from the checked item once JS has run (docs/jsx-parity.md
// `## combobox`'s own "reopening filtered to the committed label" FIX
// entry), so a no-JS request rendered an empty input despite the hidden
// bridge already posting "go" correctly, and a JS request flashed
// placeholder-to-label on load. ComboboxInput's attrs already reach the
// inner <input> (attrs.Without("class") — see its own ADAPT doc comment),
// so passing the picked item's own label through as `value` here needs no
// signature change; combobox.js's init() is now a no-op fallback for
// callers who don't supply it (it only seeds when the input is still
// empty).
component Clear() {
<ui.Combobox name="language" value="go">
<ui.ComboboxInput placeholder="Search language..." showClear value="Go" class="w-[220px]"/>
<ui.ComboboxContent>
<ui.ComboboxList>
<ui.ComboboxEmpty>No language found.</ui.ComboboxEmpty>
<ui.ComboboxItem value="go" selected={true}>Go</ui.ComboboxItem>
<ui.ComboboxItem value="rust" selected={false}>Rust</ui.ComboboxItem>
<ui.ComboboxItem value="typescript" selected={false}>TypeScript</ui.ComboboxItem>
<ui.ComboboxItem value="python" selected={false}>Python</ui.ComboboxItem>
</ui.ComboboxList>
</ui.ComboboxContent>
</ui.Combobox>
}
Trigger and clear
package combobox
import "github.com/gsxhq/gsxui/ui"
// TriggerClear exercises the one composition no other fixture reaches:
// showTrigger AND showClear together. ComboboxInput's own doc comment states
// the contract — "both true renders both buttons, with the trigger hidden by
// the input group's clear-button descendant condition whenever a clear button
// is also present (clear wins visually)", matching shadcn's own composition
// table (source map ## combobox §2) — but clear.gsx passes only showClear and
// basic.gsx only showTrigger, so nothing in the catalogue actually rendered
// both until this fixture, and the rule that hides the trigger was therefore
// invisible to the computed-style sweep and to every pin.
//
// It was in fact broken: the hiding rule is keyed on the input group
// (`:where([data-gsxui-slot-input-group]):has([…combobox-clear])
// :where([…combobox-trigger])`) and scored (0,0,0), while the trigger renders
// through InputGroupButton -> Button, whose migrated recipe emits a real
// `inline-flex` utility at (0,1,0) in the same layer. Both buttons showed.
// Combobox's own migration takes the selector to (0,3,0) as an arbitrary
// variant on the trigger's recipe class and restores upstream's composition;
// jstest/specs/layer-precedence.spec.ts pins it.
component TriggerClear() {
<ui.Combobox name="language" value="go">
<ui.ComboboxInput placeholder="Search language..." showTrigger showClear value="Go" class="w-[220px]"/>
<ui.ComboboxContent>
<ui.ComboboxList>
<ui.ComboboxEmpty>No language found.</ui.ComboboxEmpty>
<ui.ComboboxItem value="go" selected={true}>Go</ui.ComboboxItem>
<ui.ComboboxItem value="rust" selected={false}>Rust</ui.ComboboxItem>
<ui.ComboboxItem value="typescript" selected={false}>TypeScript</ui.ComboboxItem>
</ui.ComboboxList>
</ui.ComboboxContent>
</ui.Combobox>
}
Form
package combobox
import "github.com/gsxhq/gsxui/ui"
// Form demonstrates the hidden sr-only <input> form bridge (Combobox's own
// name/value params — ui/combobox.gsx's package doc comment MECHANISM
// entry): submitting this form, JS on or off, carries "framework" in the
// POST body, the same non-JS-safe guarantee ## select's own bridge gives
// ui.Select — see site/examples/input/form-row.gsx for the plain-input
// version of this pattern.
component Form() {
<form class="flex max-w-xs flex-col gap-4">
<div class="flex flex-col gap-2">
<ui.Label for="combobox-form-framework">Framework</ui.Label>
<ui.Combobox name="framework" value="">
<ui.ComboboxInput id="combobox-form-framework" placeholder="Search framework..." showTrigger/>
<ui.ComboboxContent>
<ui.ComboboxList>
<ui.ComboboxEmpty>No framework found.</ui.ComboboxEmpty>
<ui.ComboboxItem value="next.js" selected={false}>Next.js</ui.ComboboxItem>
<ui.ComboboxItem value="sveltekit" selected={false}>SvelteKit</ui.ComboboxItem>
<ui.ComboboxItem value="nuxt.js" selected={false}>Nuxt.js</ui.ComboboxItem>
<ui.ComboboxItem value="remix" selected={false}>Remix</ui.ComboboxItem>
</ui.ComboboxList>
</ui.ComboboxContent>
</ui.Combobox>
</div>
<ui.Button type="submit">Continue</ui.Button>
</form>
}
RTL
package combobox
import "github.com/gsxhq/gsxui/ui"
var categories = []struct{ Value, Label string }{
{"technology", "التكنولوجيا"},
{"design", "التصميم"},
{"business", "الأعمال"},
{"marketing", "التسويق"},
{"education", "التعليم"},
{"health", "الصحة"},
}
// Rtl mirrors shadcn's own combobox-rtl demo's Arabic category list, but
// keeps Basic's single-select shape (shadcn's version is a multi-select
// chips combobox — ComboboxChips/ComboboxChip/ComboboxValue aren't part of
// this directory's Basic and are DEVIATED from here rather than invented).
component Rtl() {
<div dir="rtl" lang="ar">
<ui.Combobox name="category" value="">
<ui.ComboboxInput placeholder="أضف فئات" showTrigger class="w-[220px]"/>
<ui.ComboboxContent>
<ui.ComboboxList>
<ui.ComboboxEmpty>لم يتم العثور على فئات.</ui.ComboboxEmpty>
{ for _, c := range categories {
<ui.ComboboxItem value={c.Value} selected={false}>{ c.Label }</ui.ComboboxItem>
} }
</ui.ComboboxList>
</ui.ComboboxContent>
</ui.Combobox>
</div>
}