Combobox
Searchable typed choices with distinct display labels and submitted values.
- Source
app/components/nitro_kit/combobox.rb- API
NitroKit::Combobox.new(id:, name:, label:, options:, value:, include_blank:)
Selection states
A named native select submits without JavaScript; enhancement adds filtering without a duplicate form value.
Selection matrix
- Denmark
- Sweden
- Norway
- Finland
- Iceland
- Denmark
- Sweden
- Norway
- Finland
- Iceland
- Denmark
- Sweden
- Norway
- Finland
- Iceland
- Denmark
- Sweden
- Norway
- Finland
- Iceland
- 1
- 2
- 3
test/dummy/app/components/gallery/components/combobox_page.rbsample("Empty optional", slug: "empty") do
render_combobox("gallery-combobox-empty", placeholder: "Choose a country")
end
sample("Selected", slug: "selected") do
render_combobox("gallery-combobox-selected", value: "dk")
end
sample("Required", slug: "required") do
render_combobox("gallery-combobox-required", required: true, placeholder: "Country required")
end
sample("Disabled", slug: "disabled") do
render_combobox("gallery-combobox-disabled", value: "se", disabled: true)
end
sample("Numeric labels", slug: "numeric-labels") do
render NitroKit::Combobox.new(
id: "gallery-combobox-numeric",
name: "deployment[retry_count]",
label: "Retry count",
options: [ [ 1, "one" ], [ 2, "two" ], [ 3, "three" ] ],
value: "two"
)
endPlacements and choice pressure
All placements and long labels retain deterministic IDs and native listbox semantics.
Placement matrix
- DevelopmentLocal data only
- StagingShared verification
- ProductionCustomer traffic
- DevelopmentLocal data only
- StagingShared verification
- ProductionCustomer traffic
- DevelopmentLocal data only
- StagingShared verification
- ProductionCustomer traffic
- DevelopmentLocal data only
- StagingShared verification
- ProductionCustomer traffic
test/dummy/app/components/gallery/components/combobox_page.rbNitroKit::Combobox::PLACEMENTS.each do |placement|
sample(placement.to_s.humanize, slug: placement.to_s) do
render NitroKit::Combobox.new(
id: "gallery-combobox-#{placement}",
name: "deployment[#{placement}]",
label: "Deployment environment",
options: ENVIRONMENTS,
value: "staging",
placement:
)
end
endForm composition
The combobox keeps one native select as its submission source alongside ordinary Nitro fields.
Deployment target
test/dummy/app/components/gallery/components/combobox_page.rbrender NitroKit::Card.new(id: "gallery-combobox-deployment-card") do |card|
card.title("Promote release", level: 3)
card.body do
form(id: "gallery-combobox-deployment-form", action: "/gallery/deployments", method: "post") do
render NitroKit::FieldGroup.new do
render NitroKit::Field.new(
nil,
:release,
id: "gallery-combobox-release",
name: "deployment[release]",
label: "Release",
value: "2026.07.13",
readonly: true
)
render NitroKit::Combobox.new(
id: "gallery-combobox-environment",
name: "deployment[environment]",
label: "Target environment",
options: ENVIRONMENTS,
value: "production",
required: true
)
render NitroKit::Button.new(
"Promote release",
id: "gallery-combobox-submit",
type: :submit,
variant: :primary
)
end
end
end
endRails form builder
`form.field(:country, as: :combobox)` puts the searchable control inside an ordinary Field with its own label, description, and errors.
Billing country
test/dummy/app/components/gallery/components/combobox_page.rbcontact = Gallery::FormExamples.billing_contact
form_with(
model: contact,
scope: :billing_contact,
url: "#billing-country",
builder: NitroKit::FormBuilder,
id: "gallery-combobox-billing-form"
) do |form|
form.group do
form.field(
:country,
as: :combobox,
id: "gallery-combobox-billing-country",
label: "Billing country",
description: "Invoices apply this country's tax rules.",
options: BILLING_COUNTRIES,
placeholder: "Search countries",
required: true
)
form.submit("Save billing country", id: "gallery-combobox-billing-save")
end
endComponent contract
Constructor options, rendered root, closed vocabularies, and compound boundary for this component, exactly as shipped.
docs/component_contracts.md · NitroKit::Combobox
- Constructor-specific options
- required
id:,name:,label:,options: value: nil,placeholder: nil,include_blank: true,placement: :bottom_start,required: false,disabled: false,autocomplete: "off",control_aria: {}
- required
- Root and closed vocabulary
div[data-nk=combobox]; placementsbottom_start bottom_end top_start top_end- Compound contract
- Options are a non-empty set of unique typed choices;
Choice#descriptionrenders as a described secondary line in the listbox. A non-nil value must match a declared option.label:renders a realLabelbound to#{id}-input;label: falserequirescontrol_aria: { label: }or{ labelledby: }and names the input, listbox, and native select from it.placeholder:is the input hint andinclude_blank:is the native blank option. The named native Select is the truthful no-JavaScript control and submission source; Stimulus reveals and synchronizes the searchable combobox enhancement.
Relevant patterns
Application conventions this component belongs to. Each summary is the leading section of its pattern document.
docs/patterns/resource_form.md
Resource form
- One model-backed Phlex component renders initial and invalid states.
- Rails owns names, values, and errors; Nitro owns presentation.
- Invalid mutations render the same model with
422; success redirects with303. - Use one primary submit: the shell toolbar owns it, or a standalone form keeps it inside
form.group. - Wrap the form in a Turbo Frame only when it needs an independent lifecycle.
System rules for coding agents
Instructions for coding agents, repeated on every component page so one fetched page has enough context. Humans can usually skip this section.
test/dummy/app/components/gallery/agent_rules.rb
View agent instructions
- Nitro Kit 2.0 is a gem-owned, Phlex-only UI system for Rails. Render components directly:
render NitroKit::Button.new("Save", variant: :primary). - Compose Nitro components inside application-owned Phlex classes. Public initializers and declared slots are the component API; private methods are not.
- Public options are explicit keywords, and enumerated options are closed vocabularies. Invalid names or values raise
ArgumentError; this component's accepted options are in the contract above. - Pass native attributes through their typed boundary:
html:for HTML,aria:for ARIA, anddata:for application data.classandstyleare forbidden, including insidehtml:. - Nitro owns
NitroKit::Component::RESERVED_DATA_ATTRIBUTES:data-nk,data-slot,data-variant,data-size,data-nk-escape,data-enhanced,data-state,data-disabled,data-required,data-orientation,data-presentation,data-placement,data-layout,data-side,data-field-type,data-dir,data-gap,data-align,data-justify,data-wrap,data-cols,data-mode,data-key. Do not pass them throughdata:.data-action,data-controllerare additive and compose with Nitro behavior. - If an integration truly requires a class, use
desperately_need_a_class:. It requires a non-blank String and marks the exception withdata-nk-escape="class". - Every root emits
data-nk; owned parts emit component-qualifieddata-slotvalues such asfield-controlorcard-title. Select on those attributes, never on classes. - Customize components with documented
--nk-*custom properties in an application stylesheet. Variables beginning with--_nk-*are private component mechanics. - Use
NitroKit::FlexandNitroKit::Gridfor layout. Parents own external placement and available width; components own their intrinsic geometry. - Preserve native elements and accessibility semantics. State is exposed through native semantics and ARIA first, and through
data-statewhen styling or behavior also needs it.