Combobox
CCombobox is a searchable single select. The submitted value must match an option. Use it when a plain Select would be too slow to scan. It does not accept arbitrary text as a value.
Combobox at a glance
Options may include supporting descriptions and disabled choices. Selection, query text, popup visibility, loading, empty, and error state stay distinct.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ComboboxAtAGlance(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section class="combo-glance">
<header>
<p>Celestial catalog</p>
<h2>Choose a destination</h2>
</header>
<div class="combo-glance__grid">
<c-CField>
<c-fill name="label">
Planet
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="planets"
value="saturn"
placeholder="Search planets"
/>
</c-fill>
</c-CField>
<c-CField>
<c-fill name="label">
Observation target
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="targets"
variant="filled"
auto_highlight
placeholder="Search targets"
/>
</c-fill>
<c-fill name="description">
Arrow keys skip unavailable targets.
</c-fill>
</c-CField>
<c-CField disabled>
<c-fill name="label">
Launch window
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="windows"
value="aurora"
variant="plain"
/>
</c-fill>
</c-CField>
</div>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"planets": (
citry_ui.CComboboxOption("mars", "Mars", "Rocky planet with a thin atmosphere"),
citry_ui.CComboboxOption("saturn", "Saturn", "Gas giant surrounded by bright rings"),
citry_ui.CComboboxOption("neptune", "Neptune", "Windy blue world in the outer system"),
),
"targets": (
citry_ui.CComboboxOption("orion", "Orion Nebula", "A bright stellar nursery"),
citry_ui.CComboboxOption("andromeda", "Andromeda Galaxy", "Nearest large spiral galaxy"),
citry_ui.CComboboxOption("carina", "Carina Nebula", "Southern-sky emission nebula", disabled=True),
),
"windows": (citry_ui.CComboboxOption("aurora", "Aurora window"),),
}
css = """
:where(.combo-glance) {
max-width: 62rem;
padding: 1.25rem;
border: 1px solid light-dark(#bfdbfe, #1e3a8a);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.combo-glance header) {
margin-block-end: 1rem;
}
:where(.combo-glance h2, .combo-glance p) {
margin: 0;
}
:where(.combo-glance header p) {
margin-block-end: 0.3rem;
color: light-dark(#1d4ed8, #93c5fd);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
:where(.combo-glance__grid) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
gap: 1rem;
align-items: start;
}
"""
preview = ComboboxAtAGlance()
preview # noqa: B018
Build a searchable single select
Pass CComboboxOption values. Add name only when the canonical value should join native FormData.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ChooseAMoon(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section class="moon-picker">
<c-CField required>
<c-fill name="label">
Moon
</c-fill>
<c-fill name="default">
<c-CCombobox
name="moon_id"
c-options="moons"
placeholder="Search moons"
/>
</c-fill>
<c-fill name="description">
Search by name, then choose one destination.
</c-fill>
<c-fill name="error">
Choose a destination from the catalog.
</c-fill>
</c-CField>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"moons": (
citry_ui.CComboboxOption("europa", "Europa", "Icy moon of Jupiter"),
citry_ui.CComboboxOption("titan", "Titan", "Moon with a dense atmosphere"),
citry_ui.CComboboxOption("triton", "Triton", "Retrograde moon of Neptune"),
citry_ui.CComboboxOption("enceladus", "Enceladus", "Bright moon with water plumes"),
)
}
css = """
:where(.moon-picker) {
max-width: 28rem;
padding: 1.25rem;
border: 1px solid light-dark(#c7d2fe, #3730a3);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
"""
preview = ChooseAMoon()
preview # noqa: B018
<c-CCombobox
name="moon_id"
c-options="moons"
placeholder="Search moons"
/>
from citry_ui import CCombobox, CComboboxOption
moon_picker = CCombobox(
name="moon_id",
options=(
CComboboxOption("europa", "Europa", "Icy moon of Jupiter"),
CComboboxOption("titan", "Titan", "Moon with a dense atmosphere"),
),
)
value is the stable identity. label is visible and filterable text. description adds optional supporting text. Duplicate labels are allowed; values must be unique.
Opening a local Combobox whose text still mirrors its selection shows all options, so the trigger can choose a replacement. Once the user edits the text, it filters normally. An explicitly controlled inputValue is always a search query.
Use CField for the accessible label, description, error, required state, and shared Form state. Do not use placeholder as the only label.
Configure Combobox
Server inputs are passed in Python through <c-CCombobox ... /> attributes or a CCombobox(...) composition call. Client inputs are passed in the browser through $c-props="{...}".
Customize example
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ConfigureCombobox(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section
class="combo-config"
x-data
x-init="Alpine.store('comboConfig', {
variant: 'outline',
size: 'md',
filter: 'contains',
clearable: true,
open_on_focus: false,
auto_highlight: false,
})"
@citry-ui-preview-controls.window="Object.assign($store.comboConfig, $event.detail)"
>
<p>Observatory controls</p>
<h2>Configure the catalog</h2>
<c-CField>
<c-fill name="label">
Deep-sky object
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="objects"
placeholder="Search the catalog"
$c-props="{
variant: $store.comboConfig.variant,
size: $store.comboConfig.size,
filter: $store.comboConfig.filter,
clearable: $store.comboConfig.clearable,
openOnFocus: $store.comboConfig.open_on_focus,
autoHighlight: $store.comboConfig.auto_highlight,
}"
/>
</c-fill>
</c-CField>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"objects": (
citry_ui.CComboboxOption("m31", "Andromeda Galaxy", "Spiral galaxy in Andromeda"),
citry_ui.CComboboxOption("m42", "Orion Nebula", "Diffuse nebula in Orion"),
citry_ui.CComboboxOption("m45", "Pleiades", "Open star cluster in Taurus"),
citry_ui.CComboboxOption("ngc7000", "North America Nebula", "Emission nebula in Cygnus"),
)
}
css = """
:where(.combo-config) {
display: grid;
gap: 0.75rem;
max-width: 38rem;
padding: 1.25rem;
border: 1px solid light-dark(#bae6fd, #075985);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.combo-config h2, .combo-config p) {
margin: 0;
}
:where(.combo-config > p) {
color: light-dark(#0369a1, #7dd3fc);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
"""
preview_controls = (
{
"name": "variant",
"label": "Variant",
"type": "select",
"default": "outline",
"options": (("outline", "Outline"), ("filled", "Filled"), ("plain", "Plain")),
},
{
"name": "size",
"label": "Size",
"type": "select",
"default": "md",
"options": (("sm", "Small"), ("md", "Medium"), ("lg", "Large")),
},
{
"name": "filter",
"label": "Local filter",
"type": "select",
"default": "contains",
"options": (("contains", "Contains"), ("starts_with", "Starts with"), ("none", "None")),
},
{"name": "clearable", "label": "Show clear action", "type": "checkbox", "default": True},
{"name": "open_on_focus", "label": "Open on focus", "type": "checkbox", "default": False},
{"name": "auto_highlight", "label": "Highlight first match", "type": "checkbox", "default": False},
)
preview = ConfigureCombobox()
preview # noqa: B018
variant, size, filter, clearable, open_on_focus, and auto_highlight have matching client inputs. A valid client input wins. Remove it to return configuration to the server value.
value, inputValue, and open behave differently: each is independently controlled while supplied. Removing query or popup control preserves its last committed state. value=null is an intentional controlled empty selection.
auto_highlight only moves the active option. It does not select on blur or Tab. min_chars applies to popup visibility and remote loading, including trigger and keyboard opening.
Search remote options
Pass loadOptions through client props. It receives the committed query, an AbortSignal, and a request ID. Return one complete valid item array.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class RemoteStarCatalog(Component):
template = """
<section
class="remote-stars"
x-data
x-init="Alpine.store('remoteStars', {
async loadStars({ query, signal }) {
await new Promise((resolve, reject) => {
const timer = setTimeout(resolve, 350);
signal.addEventListener('abort', () => {
clearTimeout(timer);
reject(new DOMException('Aborted', 'AbortError'));
}, { once: true });
});
if (query.toLowerCase() === 'offline') {
throw new Error('Catalog unavailable');
}
const stars = [
{ value: 'vega', label: 'Vega', description: 'Blue-white star in Lyra' },
{ value: 'rigel', label: 'Rigel', description: 'Blue supergiant in Orion' },
{ value: 'sirius', label: 'Sirius', description: 'Brightest star in the night sky' },
{ value: 'betelgeuse', label: 'Betelgeuse', description: 'Red supergiant in Orion' },
];
const needle = query.toLowerCase();
return stars.filter((star) => star.label.toLowerCase().includes(needle));
},
})"
>
<c-CField>
<c-fill name="label">
Star catalog
</c-fill>
<c-fill name="default">
<c-CCombobox
c-min_chars="2"
c-debounce_ms="150"
placeholder="Type at least two letters"
$c-props="{ loadOptions: $store.remoteStars.loadStars }"
>
<c-fill name="loading">
Reading the catalog...
</c-fill>
<c-fill name="empty">
No catalog match.
</c-fill>
<c-fill name="error">
The catalog could not be read.
</c-fill>
</c-CCombobox>
</c-fill>
<c-fill name="description">
Try Vega, Rigel, Sirius, or Betelgeuse. Type offline to preview recovery.
</c-fill>
</c-CField>
</section>
"""
css = """
:where(.remote-stars) {
max-width: 30rem;
padding: 1.25rem;
border: 1px solid light-dark(#a5b4fc, #4338ca);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
"""
preview = RemoteStarCatalog()
preview # noqa: B018
<c-CCombobox
c-min_chars="2"
c-debounce_ms="250"
$c-props="{
loadOptions: async ({ query, signal, requestId }) => {
const response = await fetch(`/stars?q=${encodeURIComponent(query)}`, {
signal,
});
return await response.json();
},
}"
/>
A new qualifying query aborts the previous request. Request identity still rejects stale results when a loader ignores abort. Closing, reset, disabled or read-only state, replacement, and cleanup also abort work.
Replacing loadOptions aborts its current request. A valid replacement loads the current qualifying query when the popup is open; null returns to local filtering.
Use the loading, empty, and error slots to match surrounding language. Errors never render exception text. A later valid query can recover.
Remote mode bypasses local filtering. For local data, choose contains, starts_with, or none. Matching is plain case-insensitive text matching, not locale-aware or fuzzy search.
Control browser state
Control selection, query, and popup independently. Every callback reports the affected axis, reason, ownership, and browser source.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ControlledMissionTarget(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section
class="controlled-target"
x-data
x-init="Alpine.store('missionTarget', {
value: null,
query: '',
open: false,
lastReason: 'none',
})"
>
<c-CField>
<c-fill name="label">
Mission target
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="targets"
$c-props="{
value: $store.missionTarget.value,
inputValue: $store.missionTarget.query,
open: $store.missionTarget.open,
onValueChange: (next, detail) => {
$store.missionTarget.value = next;
$store.missionTarget.lastReason = `value: ${detail.reason}`;
},
onInputValueChange: (next, detail) => {
$store.missionTarget.query = next;
$store.missionTarget.lastReason = `query: ${detail.reason}`;
},
onOpenChange: (next, detail) => {
$store.missionTarget.open = next;
$store.missionTarget.lastReason = `popup: ${detail.reason}`;
},
}"
/>
</c-fill>
</c-CField>
<dl aria-live="polite">
<div>
<dt>Value</dt>
<dd x-text="$store.missionTarget.value ?? 'none'">none</dd>
</div>
<div>
<dt>Query</dt>
<dd x-text="$store.missionTarget.query || 'empty'">empty</dd>
</div>
<div>
<dt>Last request</dt>
<dd x-text="$store.missionTarget.lastReason">none</dd>
</div>
</dl>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"targets": (
citry_ui.CComboboxOption("ceres", "Ceres", "Dwarf planet in the asteroid belt"),
citry_ui.CComboboxOption("vesta", "Vesta", "Large rocky asteroid"),
citry_ui.CComboboxOption("psyche", "16 Psyche", "Metal-rich asteroid"),
)
}
css = """
:where(.controlled-target) {
display: grid;
gap: 1rem;
max-width: 34rem;
padding: 1.25rem;
border: 1px solid light-dark(#ddd6fe, #5b21b6);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.controlled-target dl) {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0.75rem;
margin: 0;
}
:where(.controlled-target dl > div) {
min-width: 0;
padding: 0.625rem;
border-radius: 0.5rem;
background: color-mix(in srgb, CanvasText 6%, Canvas);
}
:where(.controlled-target dt) {
color: color-mix(in srgb, currentColor 65%, transparent);
font-size: 0.75rem;
}
:where(.controlled-target dd) {
margin: 0.2rem 0 0;
overflow-wrap: anywhere;
font-size: 0.875rem;
font-weight: 650;
}
"""
preview = ControlledMissionTarget()
preview # noqa: B018
<c-CCombobox
$c-props="{
value: targetId,
inputValue: targetQuery,
open: targetOpen,
onValueChange: (value, detail) => targetId = value,
onInputValueChange: (query, detail) => targetQuery = query,
onOpenChange: (open, detail) => targetOpen = open,
}"
/>
An uncontrolled axis commits before its callback. A controlled callback is a request; update the matching client input to accept it. Owner commits do not notify again. Selecting an option requests value, label query, then close in that order, but controlling one axis never takes ownership of another.
If a selected value temporarily has no item, its canonical value and last known label survive. A later matching item rehydrates the label without a callback. This supports options that arrive after selection.
Use native Forms and validation
name adds a hidden canonical input. The visible text input owns native validation but never submits its label.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class LaunchDestinationForm(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section
class="launch-form"
x-data="{ result: 'No route submitted.' }"
>
<header>
<p>Flight plan</p>
<h2>Choose a launch destination</h2>
</header>
<c-CForm
@submit.prevent="result = `Route: ${new FormData($el).get('destination_id')}`"
@reset="result = 'Flight plan reset.'"
>
<c-CField required>
<c-fill name="label">
Destination
</c-fill>
<c-fill name="default">
<c-CCombobox
name="destination_id"
c-options="destinations"
value="luna"
/>
</c-fill>
<c-fill name="error">
Choose a destination from the route catalog.
</c-fill>
</c-CField>
<div class="launch-form__actions">
<c-CButton type="submit">
Submit route
</c-CButton>
<c-CButton
type="reset"
variant="ghost"
intent="neutral"
>
Reset
</c-CButton>
</div>
</c-CForm>
<p
class="launch-form__result"
aria-live="polite"
x-text="result"
>
No route submitted.
</p>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"destinations": (
citry_ui.CComboboxOption("luna", "Lunar orbit", "Three-day transfer"),
citry_ui.CComboboxOption("mars", "Mars transfer", "Hohmann transfer window"),
citry_ui.CComboboxOption("europa", "Europa flyby", "Outer-system gravity assists"),
)
}
css = """
:where(.launch-form) {
max-width: 36rem;
padding: 1.25rem;
border: 1px solid light-dark(#bae6fd, #0c4a6e);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.launch-form header) {
margin-block-end: 1rem;
}
:where(.launch-form h2, .launch-form p) {
margin: 0;
}
:where(.launch-form header p) {
margin-block-end: 0.3rem;
color: light-dark(#0369a1, #7dd3fc);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
:where(.launch-form__actions) {
display: flex;
flex-wrap: wrap;
gap: 0.625rem;
margin-block-start: 1rem;
}
:where(.launch-form__result) {
margin-block-start: 1rem;
color: color-mix(in srgb, currentColor 70%, transparent);
font-size: 0.875rem;
}
"""
preview = LaunchDestinationForm()
preview # noqa: B018
Required validity needs a selected option, not merely typed text. Disabled Comboboxes are omitted from FormData. Read-only Comboboxes keep their value but cannot edit, open, select, or clear.
An uncanceled native reset restores uncontrolled server values. Controlled axes reassert their browser values after the reset turn. A canceled reset does nothing.
Before browser activation, the visible input is read-only. If scripts fail, the displayed label cannot change while an old hidden key is submitted. The server must still verify that every submitted key is allowed.
Browser autofill is treated as text input. It clears an old canonical value and never guesses identity from a label, including duplicate labels.
Use the keyboard
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class ConstellationKeyboardPicker(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section class="constellation-keys">
<header>
<p>Keyboard chart</p>
<h2>Navigate constellations</h2>
</header>
<c-CField>
<c-fill name="label">
Constellation
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="constellations"
open_on_focus
auto_highlight
placeholder="Search constellations"
/>
</c-fill>
<c-fill name="description">
Try Arrow keys, Home, End, Enter, Escape, and Tab.
</c-fill>
</c-CField>
<p class="constellation-keys__note">
Cetus is unavailable and is skipped by keyboard navigation.
</p>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"constellations": (
citry_ui.CComboboxOption("andromeda", "Andromeda", "Northern constellation"),
citry_ui.CComboboxOption("cetus", "Cetus", "Sea-monster constellation", disabled=True),
citry_ui.CComboboxOption("cygnus", "Cygnus", "Northern Cross"),
citry_ui.CComboboxOption("lyra", "Lyra", "Home of Vega"),
citry_ui.CComboboxOption("orion", "Orion", "Prominent winter constellation"),
)
}
css = """
:where(.constellation-keys) {
max-width: 32rem;
padding: 1.25rem;
border: 1px solid light-dark(#c4b5fd, #5b21b6);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.constellation-keys header) {
margin-block-end: 1rem;
}
:where(.constellation-keys h2, .constellation-keys p) {
margin: 0;
}
:where(.constellation-keys header p) {
margin-block-end: 0.3rem;
color: light-dark(#6d28d9, #c4b5fd);
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
:where(.constellation-keys__note) {
margin-block-start: 1rem;
color: color-mix(in srgb, currentColor 68%, transparent);
font-size: 0.8125rem;
}
"""
preview = ConstellationKeyboardPicker()
preview # noqa: B018
- ArrowDown and ArrowUp open and move across enabled options with wrap.
- Home and End move to the first or last enabled option while open.
- Enter selects the highlighted option.
- Escape closes without selecting.
- Tab closes and continues native focus order without selecting.
- Printable keys, IME, editing shortcuts, and horizontal arrows remain native.
DOM focus stays on the input. aria-activedescendant exposes the highlighted option. Pointer selection keeps input focus through the commit. The trigger and clear actions are outside sequential Tab order so the composite uses one Tab stop.
Theme and customize Combobox
Use class_, style, public CSS variables, or documented selectors. Do not target private .cui-* classes.
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class DeepSkyTheme(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section class="deep-sky-theme">
<header>
<p>Deep-sky palette</p>
<h2>Search the Messier catalog</h2>
</header>
<c-CField>
<c-fill name="label">
Messier object
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="objects"
value="m51"
class_="deep-sky-theme__picker"
/>
</c-fill>
</c-CField>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"objects": (
citry_ui.CComboboxOption("m1", "Crab Nebula", "Supernova remnant in Taurus"),
citry_ui.CComboboxOption("m42", "Orion Nebula", "Stellar nursery in Orion"),
citry_ui.CComboboxOption("m51", "Whirlpool Galaxy", "Interacting spiral galaxies"),
citry_ui.CComboboxOption("m104", "Sombrero Galaxy", "Galaxy with a bright central bulge"),
)
}
css = """
:where(.deep-sky-theme) {
--cui-combobox-background: #11142b;
--cui-combobox-foreground: #f5f3ff;
--cui-combobox-border-color: #6d5bd0;
--cui-combobox-focus-color: #f0abfc;
--cui-combobox-popup-background: #171a35;
--cui-combobox-popup-border-color: #8171d8;
--cui-combobox-highlighted-background: #312e81;
--cui-combobox-selected-background: #4c1d95;
--cui-combobox-option-description-color: #c4b5fd;
max-width: 34rem;
padding: 1.25rem;
border: 1px solid #5145a6;
border-radius: 0.875rem;
background: #0b1024;
color: #f5f3ff;
color-scheme: dark;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.deep-sky-theme header) {
margin-block-end: 1rem;
}
:where(.deep-sky-theme h2, .deep-sky-theme p) {
margin: 0;
}
:where(.deep-sky-theme header p) {
margin-block-end: 0.3rem;
color: #f0abfc;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
:where(.deep-sky-theme__picker) {
--cui-combobox-radius: 0.75rem;
--cui-combobox-popup-shadow: 0 1rem 3rem rgb(0 0 0 / 45%);
}
"""
preview = DeepSkyTheme()
preview # noqa: B018
Variables inherit, so a container can theme several Comboboxes. Set one on the root for an isolated override. Public selectors such as [data-citry-ui-part="option-description"] target stable elements. Reflected attributes such as data-open, data-loading, data-selected, and data-highlighted expose current styling state.
The popup stays under the component and inherits its theme. It does not use the browser top layer yet, so an ancestor with clipped overflow may clip it.
Support narrow, translated, and directional content
Show code
import citry_ui
from citry import Component, citry
citry.register_library(citry_ui)
class CelestialNamesEnvironment(Component):
class Kwargs:
pass
class Slots:
pass
template = """
<section class="celestial-environment">
<div
dir="rtl"
style="color-scheme: dark"
>
<c-CField>
<c-fill name="label">
جرم سماوي
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="arabic_objects"
value="thurayya"
size="sm"
/>
</c-fill>
</c-CField>
</div>
<div
class="celestial-environment__narrow"
style="color-scheme: light"
>
<c-CField>
<c-fill name="label">
Long catalog name
</c-fill>
<c-fill name="default">
<c-CCombobox
c-options="long_names"
open_on_focus
placeholder="Search long names"
/>
</c-fill>
</c-CField>
</div>
</section>
"""
def template_data(
self,
kwargs: Kwargs, # noqa: ARG002
slots: Slots, # noqa: ARG002
) -> dict[str, object]:
return {
"arabic_objects": (
citry_ui.CComboboxOption("thurayya", "الثريا", "عنقود نجمي مفتوح"),
citry_ui.CComboboxOption("jauza", "الجوزاء", "كوكبة بارزة في سماء الشتاء"),
),
"long_names": (
citry_ui.CComboboxOption(
"andromeda-satellite",
"Andromeda Galaxy satellite candidate in the outer stellar halo",
"A deliberately long label that wraps instead of covering the action controls",
),
citry_ui.CComboboxOption(
"magellanic-stream",
"Magellanic Stream high-velocity cloud observation",
"Supporting text also wraps inside a narrow popup",
),
),
}
css = """
:where(.celestial-environment) {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
gap: 1rem;
align-items: start;
max-width: 48rem;
padding: 1.25rem;
border: 1px solid light-dark(#bfdbfe, #1e40af);
border-radius: 0.875rem;
background: Canvas;
color: CanvasText;
font-family: ui-sans-serif, system-ui, sans-serif;
}
:where(.celestial-environment__narrow) {
max-width: 17rem;
}
"""
preview = CelestialNamesEnvironment()
preview # noqa: B018
Logical properties support RTL. Labels and descriptions wrap inside the scrollable popup. Default colors support light and dark schemes and retain boundaries and highlight in forced colors.
Version 1 targets ordinary collections up to 1,000 items. Grouping, virtualization, infinite loading, multiple selection, free values, create-new, and arbitrary option rendering remain separate later work.
API reference
Inputs
CCombobox server inputs
Server inputs are passed in a template through <c-CCombobox ... /> or in Python through CCombobox(...).
| Input | Type | Default | Effect |
|---|---|---|---|
options | Sequence[CComboboxOption] | "()" | Supplies the initial strict-selection collection. |
name | str | None | None | Adds the optional hidden native submitted name. Omit it when the control does not participate in a Form. |
id | str | None | Uses the Field ID or a generated ID. | Sets visible-input, listbox, and option identity. |
value | str | None | None | Sets initial canonical selection. A value may arrive before its matching item. |
input_value | str | None | Uses the selected label or an empty string. | Sets initial editable query text. |
open | bool | False | Sets initial popup visibility when interaction and the query threshold allow it. |
required | bool | None | None | Requires canonical selection when standalone; CField owns this state when composed. |
disabled | bool | None | None | Disables interaction and Form participation when standalone; disabled CForm always wins and CField owns this state when composed. |
readonly | bool | None | Inherits CForm when standalone. | Preserves Form participation but blocks editing, opening, clearing, and selection; CField owns this state when composed. |
invalid | bool | None | None | Sets application invalid presentation when standalone; CField owns this state when composed. |
loading | bool | False | Adds external loading presentation to internal remote loading. |
clearable | bool | True | Shows the clear action when selection or query text exists. |
open_on_focus | bool | False | Opens on input focus when the query meets min_chars. |
auto_highlight | bool | False | Highlights the first enabled match after filtering or loading without selecting it on blur or Tab. |
filter | "contains" | "starts_with" | "none" (CComboboxFilter) | "contains" | Selects plain case-insensitive local matching. Remote mode bypasses it. |
min_chars | non-negative int | 0 | Sets the minimum Unicode-code-point query length for popup visibility and remote loading. |
debounce_ms | non-negative int | 200 | Sets remote request delay in milliseconds. |
placeholder | str | None | None | Sets the visible native-input placeholder. |
autocomplete | non-empty str | "off" | Sets the visible native-input autocomplete hint. Browser heuristics may still override it. |
inputmode | str | None | None | Sets the visible native-input virtual-keyboard hint. |
required_message | non-empty str | "Select an option." | Sets native custom-validity text for a missing required selection. |
clear_label | non-empty str | "Clear selection" | Names the clear Button. |
open_label | non-empty str | "Show options" | Names the closed popup trigger. |
close_label | non-empty str | "Hide options" | Names the open popup trigger. |
loading_label | non-empty str | "Loading options..." | Sets fallback loading status text. |
empty_label | non-empty str | "No options found." | Sets fallback empty status text. |
error_label | non-empty str | "Options could not be loaded." | Sets fallback remote-error status text. |
variant | "outline" | "filled" | "plain" (CComboboxVariant) | "outline" | Selects control presentation. |
size | "sm" | "md" | "lg" (CComboboxSize) | "md" | Selects control geometry. |
class_ | str | Mapping[str, bool] | Sequence[CClassValue] | None (CClassValue) | None | Adds root classes and merges them with attrs. |
style | str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None (CStyleValue) | None | Adds root inline styles and merges them with attrs. |
attrs | Mapping[str, object] | None | None | Adds allowed native, ARIA, Alpine, and data attributes to the root. Prefer the top-level class and style inputs. |
input_attrs | Mapping[str, object] | None | None | Adds allowed attributes to the visible input. form is rejected so validation and submitted value cannot have different owners. |
CCombobox client inputs
Client inputs are passed in the browser through the $c-props="{ ... }" attribute on <c-CCombobox />.
| Input | Type | Omitted behavior | Effect |
|---|---|---|---|
items | CComboboxItem[] (CComboboxItem) | Keeps the current collection. | Replaces the current collection while valid. null, malformed entries, and duplicate values retain the prior collection and report once per invalid episode. |
value | non-empty string | null | Continues uncontrolled from the current selection. | Controls canonical selection while supplied. null is an intentional empty controlled value; empty strings and other invalid values release control from current state. |
inputValue | string | null | Continues uncontrolled from the current query. null has the same effect. | Controls editable query text while supplied as a string. Invalid values report and release control. |
open | boolean | null | Continues uncontrolled from current visibility. null has the same effect. | Controls requested popup visibility; disabled, read-only, and query-threshold rules still determine effective visibility. |
required | boolean | Uses the server value. | Controls required state only when standalone. CField owns it when composed. |
disabled | boolean | Uses the server value. | Controls local disabled state only when standalone. Disabled CForm always wins and CField owns it when composed. |
readonly | boolean | Uses the server or reactive CForm value. | Controls read-only state only when standalone. CField owns it when composed. |
invalid | boolean | Uses the server value. | Controls application invalid state only when standalone. CField owns it when composed. |
loading | boolean | Uses the server input. | Adds external loading presentation. |
clearable | boolean | Uses the server input. | Controls clear-action visibility. |
openOnFocus | boolean | Uses the server input. | Controls focus-triggered opening. |
autoHighlight | boolean | Uses the server input. | Controls first-match highlight after filtering or loading. |
filter | "contains" | "starts_with" | "none" (CComboboxFilter) | Uses the server input. | Controls local matching. |
minChars | non-negative integer | Uses the server input. | Controls the Unicode-code-point popup and remote-query threshold. |
debounceMs | non-negative integer | Uses the server input. | Controls remote delay. |
variant | "outline" | "filled" | "plain" (CComboboxVariant) | Uses the server input. | Controls presentation. |
size | "sm" | "md" | "lg" (CComboboxSize) | Uses the server input. | Controls geometry. |
loadOptions | (request: CComboboxLoadRequest) => Promise<CComboboxItem[]> | CComboboxItem[] (CComboboxLoadRequest, CComboboxItem) | Uses local filtering. | Loads one complete replacement collection for the committed query. null disables remote mode. Replacing or removing the function aborts current work; a valid replacement reloads a qualifying open query. |
onValueChange | function | Does not notify a selection callback. | Receives user and reset selection requests. |
onInputValueChange | function | Does not notify a query callback. | Receives user and reset query requests. |
onOpenChange | function | Does not notify a visibility callback. | Receives user-authored popup requests and query-threshold closure. |
onLoadError | function | Does not notify a loading-error callback. | Receives current non-abort remote failures after safe error presentation. |
Slots
Slots are passed as nested content or <c-fill> tags in a template, or through the slots={...} argument in Python.
CCombobox slots
| Slot | Required | Data | Fallback |
|---|---|---|---|
loading | no | {} (CComboboxLoadingSlotData) | loading_label |
empty | no | {} (CComboboxEmptySlotData) | empty_label |
error | no | {} (CComboboxErrorSlotData) | error_label |
Events
Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.
CCombobox events
| Event | Signature | Trigger and timing | Detail | Controlled and cancellation behavior |
|---|---|---|---|---|
onValueChange | (value: string | null, detail: CComboboxValueChangeDetail) => void (CComboboxValueChangeDetail) | Option selection, clear, text invalidation, or uncanceled reset requests a different canonical value. | {reason: "option" | "clear" | "input" | "reset", option: CComboboxItem | null, query: string, controlled: boolean, source: EventTarget | null} (CComboboxValueChangeDetail, CComboboxItem) | Uncontrolled value and Form state commit before notification. Controlled requests wait for the owner. Owner commits and repeated values do not notify. |
onInputValueChange | (query: string, detail: CComboboxInputValueChangeDetail) => void (CComboboxInputValueChangeDetail) | Input, option, clear, blur reconciliation, or uncanceled reset requests different query text. | {reason: "input" | "option" | "clear" | "blur" | "reset", controlled: boolean, source: EventTarget | null} (CComboboxInputValueChangeDetail) | Uncontrolled visible text commits before notification. Controlled requests wait for the owner. Owner commits do not notify. |
onOpenChange | (open: boolean, detail: CComboboxOpenChangeDetail) => void (CComboboxOpenChangeDetail) | Input, focus, trigger, keyboard, selection, Escape, outside press, blur, reset, or threshold requests different visibility. | {reason: "input" | "focus" | "trigger" | "keyboard" | "selection" | "escape" | "outside" | "blur" | "reset" | "minimum-characters", controlled: boolean, source: EventTarget | null} (CComboboxOpenChangeDetail) | Uncontrolled visibility and ARIA commit before notification. Controlled requests wait for the owner. Owner commits do not notify. |
onLoadError | (error: unknown, detail: CComboboxLoadErrorDetail) => void (CComboboxLoadErrorDetail) | The current remote loader throws, rejects, or returns an invalid collection for a reason other than abort. | {query: string, requestId: number} (CComboboxLoadErrorDetail) | Error presentation appears before notification. Abort and stale requests do not notify. |
Methods
-
CSS
CSS variables to theme the components. Set them on an ancestor or the component itself.
CCombobox CSS variables
Apply these variables to CCombobox or one of its ancestors.
| Variable | Type | Purpose | Default |
|---|---|---|---|
--cui-combobox-background | color | Control background. | Canvas |
--cui-combobox-foreground | color | Control and popup text. | CanvasText |
--cui-combobox-border-color | color | Resting control border. | Subtle CanvasText mix. |
--cui-combobox-focus-color | color | Focus ring and border. | Highlight |
--cui-combobox-invalid-color | color | Invalid border. | Scheme-aware error color. |
--cui-combobox-radius | length | Control, popup, and option radius basis. | 0.5rem |
--cui-combobox-height | length | Minimum control height. | Size-derived. |
--cui-combobox-inline-padding | length | Input logical inline padding. | Size-derived. |
--cui-combobox-icon-size | length | Clear and trigger Button size. | 2.25rem |
--cui-combobox-popup-background | color | Popup surface. | Canvas |
--cui-combobox-popup-border-color | color | Popup border. | Subtle CanvasText mix. |
--cui-combobox-popup-shadow | shadow | Popup elevation. | 0 0.75rem 2rem rgb(15 23 42 / 18%) |
--cui-combobox-popup-max-height | length | Scrollable list height. | 18rem |
--cui-combobox-option-padding | length | Option padding. | 0.625rem 0.75rem |
--cui-combobox-option-gap | length | Gap between option label and description. | 0.125rem |
--cui-combobox-option-description-color | color | Supporting option text. | Muted current color. |
--cui-combobox-highlighted-background | color | Keyboard or pointer highlight. | Subtle Highlight/Canvas mix. |
--cui-combobox-selected-background | color | Committed selected-option background. | Stronger Highlight/Canvas mix. |
--cui-combobox-disabled-opacity | number | Disabled control and option opacity. | 0.55 |
--cui-combobox-error-color | color | Remote error text. | Scheme-aware error color. |
Attributes
HTML attributes defined on the components that you can refer to for CSS, inspection, and testing. Read-only.
CCombobox attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
data-open | Root | present | absent | Mirrors effective popup visibility. |
data-loading | Root | present | absent | Mirrors external or current remote loading. |
data-empty | Root | present | absent | Mirrors visible open empty state. |
data-error | Root | present | absent | Mirrors current remote-error state. |
data-required | Root | present | absent | Mirrors effective required state. |
data-disabled | Root | present | absent | Mirrors effective disabled state. |
data-readonly | Root | present | absent | Mirrors effective read-only state. |
data-invalid | Root | present | absent | Mirrors application or native invalid state. |
data-variant | Root | "outline" | "filled" | "plain" | Mirrors effective variant. |
data-size | Root | "sm" | "md" | "lg" | Mirrors effective size. |
CCombobox attributes
| Attribute | Element | Type | Meaning |
|---|---|---|---|
data-value | Option | string | Canonical option identity. |
data-selected | Option | present | absent | Mirrors committed selection. |
data-highlighted | Option | present | absent | Mirrors transient keyboard or pointer highlight. |
data-disabled | Option | present | absent | Mirrors disabled state. |
Selectors
Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.
CCombobox selectors
| Selector | Element | Purpose |
|---|---|---|
[data-citry-ui-part="root"] | Root | Combobox root and attrs destination. |
[data-citry-ui-part="control"] | Control | Visible input and action layout. |
[data-citry-ui-part="input"] | Native text input | Editable query, validation owner, and input_attrs destination. |
[data-citry-ui-part="clear"] | Clear Button | Clear action. |
[data-citry-ui-part="trigger"] | Trigger Button | Popup toggle. |
[data-citry-ui-part="popup"] | Popup | Inline popup surface. |
[data-citry-ui-part="listbox"] | Listbox | ARIA listbox and scrolling collection. |
[data-citry-ui-part="option"] | Option | Selectable plain-text item. |
[data-citry-ui-part="option-label"] | Option label | Primary visible text. |
[data-citry-ui-part="option-description"] | Option description | Optional supporting visible text. |
[data-citry-ui-part="loading"] | Loading status | Remote or external loading feedback. |
[data-citry-ui-part="empty"] | Empty status | Open empty-result feedback. |
[data-citry-ui-part="error"] | Error status | Safe remote-failure feedback. |
Interfaces
Aliases and data shapes referenced above.
Input type aliases
| Interface | Definition |
|---|---|
CClassValue | str | Mapping[str, bool] | Sequence[CClassValue] |
CStyleValue | str | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] |
CComboboxFilter | Literal["contains", "starts_with", "none"] |
CComboboxVariant | Literal["outline", "filled", "plain"] |
CComboboxSize | Literal["sm", "md", "lg"] |
CComboboxOption
| Field | Type | Default | Meaning |
|---|---|---|---|
value | non-empty str | - | Stable identity and optional submitted value. |
label | non-empty str | - | Primary escaped visible text and local filter text. |
description | str | None | None | Optional escaped supporting text. |
disabled | bool | False | Excludes the option from selection and highlight. |
CComboboxItem
| Field | Type | Default | Meaning |
|---|---|---|---|
value | non-empty string | - | Stable unique identity and canonical value. |
label | non-empty string | - | Primary plain text and local filter text. |
description | string | null | null | Optional supporting plain text. |
disabled | boolean | false | Excludes the item from selection and highlight. |
CComboboxLoadRequest
| Field | Type | Default | Meaning |
|---|---|---|---|
query | string | - | Committed query that qualified for loading. |
signal | AbortSignal | - | Aborts when superseded, closed, reset, blocked, replaced, or removed. |
requestId | number | - | Monotonic identity used to reject stale results even if abort is ignored. |
CComboboxLoadingSlotData
Empty dataclass: {}.
CComboboxEmptySlotData
Empty dataclass: {}.
CComboboxErrorSlotData
Empty dataclass: {}.
CComboboxValueChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | "option" | "clear" | "input" | "reset" | - | Canonical-value request source. |
option | CComboboxItem | null | - | Associated option when one caused the request. |
query | string | - | Query before dependent query reconciliation. |
controlled | boolean | - | Whether a valid client value currently owns selection. |
source | EventTarget | null | - | Browser source associated with the request. |
CComboboxInputValueChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | "input" | "option" | "clear" | "blur" | "reset" | - | Query request source. |
controlled | boolean | - | Whether a valid client inputValue currently owns query text. |
source | EventTarget | null | - | Browser source associated with the request. |
CComboboxOpenChangeDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
reason | "input" | "focus" | "trigger" | "keyboard" | "selection" | "escape" | "outside" | "blur" | "reset" | "minimum-characters" | - | Popup request source. |
controlled | boolean | - | Whether a valid client open currently owns requested visibility. |
source | EventTarget | null | - | Browser source associated with the request. |
CComboboxLoadErrorDetail
| Field | Type | Default | Meaning |
|---|---|---|---|
query | string | - | Query associated with the failed current request. |
requestId | number | - | Failed request identity. |
Translation keys
Catalog keys used by this family. An explicit component input or slot listed in Override takes precedence over the catalog for that instance.
CCombobox translation keys
| Key | Purpose | Variables | Override | Browser updates |
|---|---|---|---|---|
citry-ui-combobox-required | Supplies native required-selection validity text. | None | required_message input | i18n.bind() updates the native validation message. |
citry-ui-combobox-clear | Names the clear-selection control. | None | clear_label input | $c-tr updates aria-label. |
citry-ui-combobox-open | Names the trigger while the popup is closed. | None | open_label input | i18n.bind() updates the stateful aria-label. |
citry-ui-combobox-close | Names the trigger while the popup is open. | None | close_label input | i18n.bind() updates the stateful aria-label. |
citry-ui-combobox-loading | Reports asynchronous option loading. | None | loading_label input or loading slot | $c-tr updates fallback text. |
citry-ui-combobox-empty | Reports an empty option result. | None | empty_label input or empty slot | $c-tr updates fallback text. |
citry-ui-combobox-error | Reports option-loading failure. | None | error_label input or error slot | $c-tr updates fallback text. |