Combobox

Basic

Next.js
SvelteKit
Nuxt.js
Remix
Astro
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

Citrus
Orange
Lemon
Lime
Berries
Strawberry
Blueberry
Raspberry
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

Go
Rust
TypeScript
Python
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

Go
Rust
TypeScript
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

Next.js
SvelteKit
Nuxt.js
Remix
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>
}