Theme
Version
GitHub PyPI Discord
On this page

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.

Combobox at a glance
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.

Choose a moon
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="{...}".

Configure Combobox
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.

Search a star catalog
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.

Control a mission target
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.

Submit a launch destination
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

Navigate constellations
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.

Theme a deep-sky picker
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

Explore long celestial names
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(...).

InputTypeDefaultEffect
optionsSequence[CComboboxOption]"()"Supplies the initial strict-selection collection.
namestr | NoneNoneAdds the optional hidden native submitted name. Omit it when the control does not participate in a Form.
idstr | NoneUses the Field ID or a generated ID.Sets visible-input, listbox, and option identity.
valuestr | NoneNoneSets initial canonical selection. A value may arrive before its matching item.
input_valuestr | NoneUses the selected label or an empty string.Sets initial editable query text.
openboolFalseSets initial popup visibility when interaction and the query threshold allow it.
requiredbool | NoneNoneRequires canonical selection when standalone; CField owns this state when composed.
disabledbool | NoneNoneDisables interaction and Form participation when standalone; disabled CForm always wins and CField owns this state when composed.
readonlybool | NoneInherits CForm when standalone.Preserves Form participation but blocks editing, opening, clearing, and selection; CField owns this state when composed.
invalidbool | NoneNoneSets application invalid presentation when standalone; CField owns this state when composed.
loadingboolFalseAdds external loading presentation to internal remote loading.
clearableboolTrueShows the clear action when selection or query text exists.
open_on_focusboolFalseOpens on input focus when the query meets min_chars.
auto_highlightboolFalseHighlights 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_charsnon-negative int0Sets the minimum Unicode-code-point query length for popup visibility and remote loading.
debounce_msnon-negative int200Sets remote request delay in milliseconds.
placeholderstr | NoneNoneSets the visible native-input placeholder.
autocompletenon-empty str"off"Sets the visible native-input autocomplete hint. Browser heuristics may still override it.
inputmodestr | NoneNoneSets the visible native-input virtual-keyboard hint.
required_messagenon-empty str"Select an option."Sets native custom-validity text for a missing required selection.
clear_labelnon-empty str"Clear selection"Names the clear Button.
open_labelnon-empty str"Show options"Names the closed popup trigger.
close_labelnon-empty str"Hide options"Names the open popup trigger.
loading_labelnon-empty str"Loading options..."Sets fallback loading status text.
empty_labelnon-empty str"No options found."Sets fallback empty status text.
error_labelnon-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)NoneAdds root classes and merges them with attrs.
stylestr | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue] | None (CStyleValue)NoneAdds root inline styles and merges them with attrs.
attrsMapping[str, object] | NoneNoneAdds allowed native, ARIA, Alpine, and data attributes to the root. Prefer the top-level class and style inputs.
input_attrsMapping[str, object] | NoneNoneAdds 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 />.

InputTypeOmitted behaviorEffect
itemsCComboboxItem[] (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.
valuenon-empty string | nullContinues 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.
inputValuestring | nullContinues 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.
openboolean | nullContinues uncontrolled from current visibility. null has the same effect.Controls requested popup visibility; disabled, read-only, and query-threshold rules still determine effective visibility.
requiredbooleanUses the server value.Controls required state only when standalone. CField owns it when composed.
disabledbooleanUses the server value.Controls local disabled state only when standalone. Disabled CForm always wins and CField owns it when composed.
readonlybooleanUses the server or reactive CForm value.Controls read-only state only when standalone. CField owns it when composed.
invalidbooleanUses the server value.Controls application invalid state only when standalone. CField owns it when composed.
loadingbooleanUses the server input.Adds external loading presentation.
clearablebooleanUses the server input.Controls clear-action visibility.
openOnFocusbooleanUses the server input.Controls focus-triggered opening.
autoHighlightbooleanUses the server input.Controls first-match highlight after filtering or loading.
filter"contains" | "starts_with" | "none" (CComboboxFilter)Uses the server input.Controls local matching.
minCharsnon-negative integerUses the server input.Controls the Unicode-code-point popup and remote-query threshold.
debounceMsnon-negative integerUses 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.
onValueChangefunctionDoes not notify a selection callback.Receives user and reset selection requests.
onInputValueChangefunctionDoes not notify a query callback.Receives user and reset query requests.
onOpenChangefunctionDoes not notify a visibility callback.Receives user-authored popup requests and query-threshold closure.
onLoadErrorfunctionDoes 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

SlotRequiredDataFallback
loadingno{} (CComboboxLoadingSlotData)loading_label
emptyno{} (CComboboxEmptySlotData)empty_label
errorno{} (CComboboxErrorSlotData)error_label

Events

Component events are callback inputs supplied through $c-props. Native browser events remain available through Alpine @... attributes.

CCombobox events

EventSignatureTrigger and timingDetailControlled 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.

VariableTypePurposeDefault
--cui-combobox-backgroundcolorControl background.Canvas
--cui-combobox-foregroundcolorControl and popup text.CanvasText
--cui-combobox-border-colorcolorResting control border.Subtle CanvasText mix.
--cui-combobox-focus-colorcolorFocus ring and border.Highlight
--cui-combobox-invalid-colorcolorInvalid border.Scheme-aware error color.
--cui-combobox-radiuslengthControl, popup, and option radius basis.0.5rem
--cui-combobox-heightlengthMinimum control height.Size-derived.
--cui-combobox-inline-paddinglengthInput logical inline padding.Size-derived.
--cui-combobox-icon-sizelengthClear and trigger Button size.2.25rem
--cui-combobox-popup-backgroundcolorPopup surface.Canvas
--cui-combobox-popup-border-colorcolorPopup border.Subtle CanvasText mix.
--cui-combobox-popup-shadowshadowPopup elevation.0 0.75rem 2rem rgb(15 23 42 / 18%)
--cui-combobox-popup-max-heightlengthScrollable list height.18rem
--cui-combobox-option-paddinglengthOption padding.0.625rem 0.75rem
--cui-combobox-option-gaplengthGap between option label and description.0.125rem
--cui-combobox-option-description-colorcolorSupporting option text.Muted current color.
--cui-combobox-highlighted-backgroundcolorKeyboard or pointer highlight.Subtle Highlight/Canvas mix.
--cui-combobox-selected-backgroundcolorCommitted selected-option background.Stronger Highlight/Canvas mix.
--cui-combobox-disabled-opacitynumberDisabled control and option opacity.0.55
--cui-combobox-error-colorcolorRemote 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

AttributeElementTypeMeaning
data-openRootpresent | absentMirrors effective popup visibility.
data-loadingRootpresent | absentMirrors external or current remote loading.
data-emptyRootpresent | absentMirrors visible open empty state.
data-errorRootpresent | absentMirrors current remote-error state.
data-requiredRootpresent | absentMirrors effective required state.
data-disabledRootpresent | absentMirrors effective disabled state.
data-readonlyRootpresent | absentMirrors effective read-only state.
data-invalidRootpresent | absentMirrors application or native invalid state.
data-variantRoot"outline" | "filled" | "plain"Mirrors effective variant.
data-sizeRoot"sm" | "md" | "lg"Mirrors effective size.

CCombobox attributes

AttributeElementTypeMeaning
data-valueOptionstringCanonical option identity.
data-selectedOptionpresent | absentMirrors committed selection.
data-highlightedOptionpresent | absentMirrors transient keyboard or pointer highlight.
data-disabledOptionpresent | absentMirrors disabled state.

Selectors

Selectors for the DOM nodes in the components that you can use for CSS, inspection, and testing.

CCombobox selectors

SelectorElementPurpose
[data-citry-ui-part="root"]RootCombobox root and attrs destination.
[data-citry-ui-part="control"]ControlVisible input and action layout.
[data-citry-ui-part="input"]Native text inputEditable query, validation owner, and input_attrs destination.
[data-citry-ui-part="clear"]Clear ButtonClear action.
[data-citry-ui-part="trigger"]Trigger ButtonPopup toggle.
[data-citry-ui-part="popup"]PopupInline popup surface.
[data-citry-ui-part="listbox"]ListboxARIA listbox and scrolling collection.
[data-citry-ui-part="option"]OptionSelectable plain-text item.
[data-citry-ui-part="option-label"]Option labelPrimary visible text.
[data-citry-ui-part="option-description"]Option descriptionOptional supporting visible text.
[data-citry-ui-part="loading"]Loading statusRemote or external loading feedback.
[data-citry-ui-part="empty"]Empty statusOpen empty-result feedback.
[data-citry-ui-part="error"]Error statusSafe remote-failure feedback.

Interfaces

Aliases and data shapes referenced above.

Input type aliases

InterfaceDefinition
CClassValuestr | Mapping[str, bool] | Sequence[CClassValue]
CStyleValuestr | Mapping[str, str | int | float | bool | None] | Sequence[CStyleValue]
CComboboxFilterLiteral["contains", "starts_with", "none"]
CComboboxVariantLiteral["outline", "filled", "plain"]
CComboboxSizeLiteral["sm", "md", "lg"]

CComboboxOption

FieldTypeDefaultMeaning
valuenon-empty str-Stable identity and optional submitted value.
labelnon-empty str-Primary escaped visible text and local filter text.
descriptionstr | NoneNoneOptional escaped supporting text.
disabledboolFalseExcludes the option from selection and highlight.

CComboboxItem

FieldTypeDefaultMeaning
valuenon-empty string-Stable unique identity and canonical value.
labelnon-empty string-Primary plain text and local filter text.
descriptionstring | nullnullOptional supporting plain text.
disabledbooleanfalseExcludes the item from selection and highlight.

CComboboxLoadRequest

FieldTypeDefaultMeaning
querystring-Committed query that qualified for loading.
signalAbortSignal-Aborts when superseded, closed, reset, blocked, replaced, or removed.
requestIdnumber-Monotonic identity used to reject stale results even if abort is ignored.

CComboboxLoadingSlotData

Empty dataclass: {}.

CComboboxEmptySlotData

Empty dataclass: {}.

CComboboxErrorSlotData

Empty dataclass: {}.

CComboboxValueChangeDetail

FieldTypeDefaultMeaning
reason"option" | "clear" | "input" | "reset"-Canonical-value request source.
optionCComboboxItem | null-Associated option when one caused the request.
querystring-Query before dependent query reconciliation.
controlledboolean-Whether a valid client value currently owns selection.
sourceEventTarget | null-Browser source associated with the request.

CComboboxInputValueChangeDetail

FieldTypeDefaultMeaning
reason"input" | "option" | "clear" | "blur" | "reset"-Query request source.
controlledboolean-Whether a valid client inputValue currently owns query text.
sourceEventTarget | null-Browser source associated with the request.

CComboboxOpenChangeDetail

FieldTypeDefaultMeaning
reason"input" | "focus" | "trigger" | "keyboard" | "selection" | "escape" | "outside" | "blur" | "reset" | "minimum-characters"-Popup request source.
controlledboolean-Whether a valid client open currently owns requested visibility.
sourceEventTarget | null-Browser source associated with the request.

CComboboxLoadErrorDetail

FieldTypeDefaultMeaning
querystring-Query associated with the failed current request.
requestIdnumber-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

KeyPurposeVariablesOverrideBrowser updates
citry-ui-combobox-requiredSupplies native required-selection validity text.Nonerequired_message inputi18n.bind() updates the native validation message.
citry-ui-combobox-clearNames the clear-selection control.Noneclear_label input$c-tr updates aria-label.
citry-ui-combobox-openNames the trigger while the popup is closed.Noneopen_label inputi18n.bind() updates the stateful aria-label.
citry-ui-combobox-closeNames the trigger while the popup is open.Noneclose_label inputi18n.bind() updates the stateful aria-label.
citry-ui-combobox-loadingReports asynchronous option loading.Noneloading_label input or loading slot$c-tr updates fallback text.
citry-ui-combobox-emptyReports an empty option result.Noneempty_label input or empty slot$c-tr updates fallback text.
citry-ui-combobox-errorReports option-loading failure.Noneerror_label input or error slot$c-tr updates fallback text.