Fandry UI
Forms

Combobox

A searchable select — type to narrow the options, then pick one.

fandry-combobox

Example

Link to section Example

Custom colors and parts

Link to section Custom colors and parts

The same component with a few --fd-* tokens set around it and its parts restyled. The code is under Parts below.

Props

Link to section Props
PropTypeDefaultDescription
labelstring''Visible label.
placeholderstring''Shown when nothing is selected or typed.
help-textstring''Helper text below the field.
error-textstring''An error shown under the field: the field is invalid while it has one (aria-invalid, the invalid state), and a screen reader hears it when it appears and when the field is focused.
namestring''Exposed as data-name on the input.
options{ label, value, description?, group?, keywords?, disabled? }[][]The options. Search matches label, description, keywords and group; a prefix in the label ranks first.
valuestring''Selected value. Listen for change (detail is the new value).
disabledbooleanfalseDisables the field.
requiredbooleanfalseMarks the field required (asterisk + aria-required).
element-propsRecord<string, unknown>{}Spread onto the native input (e.g. { tabIndex: 2 }); keys the component controls are ignored with a warning.
empty (slot)slot'No results'Replaces the message shown when nothing matches.
label · help-text · error-text (slots)slotthe propsMarkup in place of the label, help text or error text. Shown even without the matching prop.

Parts

Link to section Parts

Style the elements inside the component from your own CSS with ::part(). Every fandry component renders in native shadow DOM, so parts work on LWR and Salesforce alike. For colors, spacing, radius and type across many components, set the --fd-* tokens instead. The code for the "Custom colors and parts" example above:

PartDescription
baseThe outermost element.
labelThe label.
requiredThe required-field asterisk.
controlThe box the field draws: a text field's border, a select's button, a checkbox's box, a radio's circle, a switch's track.
inputThe native <input>.
chevronThe dropdown arrow.
panelThe surface that opens: a dropdown's list, a popover, a tooltip, a dialog, a loading overlay's box.
listboxThe list of options.
groupA group of options.
group-labelA group's heading.
optionOne option.
option-labelAn option's label.
option-descriptionAn option's second line.
emptyWhat shows when there is nothing to list.
help-textThe help text under the field.
error-textThe error under the field. In the page even while empty, so style it only for a field that is invalid.

States are extra names on a part while the state is on. Name both to style one state, e.g.fandry-checkbox::part(control checked).

StateDescription
disabledA control, option or item that cannot be used.
invalidA field that has error text.
selectedThe chosen option, a selected table row, or the selected tab.
activeThe option the keyboard or pointer is on.
<!-- template -->
<div class="brand narrow">
  <fandry-combobox
    label="Category"
    placeholder="Search categories"
    options={options}
    value={value}
    onchange={handleChange}
  ></fandry-combobox>
</div>

/* css */
/* Scoped to this demo. Set the same --fd-* tokens on :root to theme
   every fandry-* component on the site. */
.brand {
  --fd-radius-md: 0.75rem;
  --fd-border-focus: 160 84% 36%;
  --fd-ring-color: 160 84% 36%;
  --fd-bg-muted: 160 40% 94%;
}

fandry-combobox::part(control) {
  background: hsl(160 40% 98%);
  font-weight: 600;
}

fandry-combobox::part(group-label) {
  color: hsl(160 84% 26%);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

fandry-combobox::part(option-description) {
  font-style: italic;
}
​

Code

Link to section Code
<fandry-combobox
  label="Framework"
  placeholder="Search frameworks"
  options={frameworkOptions}
  value={value}
  onchange={handleChange}
></fandry-combobox>
​

Example: Custom rows and empty state

Link to section Example: Custom rows and empty state

Code

<!-- template -->
<fandry-combobox label="Plan" options={planOptions}
  value={value} onchange={handleChange}>
  <span slot="empty">No plan matches.</span>
</fandry-combobox>

// component
import PlanRow from 'my/planRow'; // your LWC: @api icon, label, hint

planOptions = [
  {
    label: 'Free', value: 'free', component: PlanRow,
    componentProps: { icon: '🌱', label: 'Free' }
  },
  {
    label: 'Pro', value: 'pro', component: PlanRow,
    componentProps: { icon: '💎', label: 'Pro', hint: 'Popular' }
  }
];

// The component draws only the inside of the row; the row keeps its
// highlight, hover and click. Search still matches on `label`
// (and keywords/description), so give every item a real one.
​

Example: Build on the base class: own template, async search

Link to section Example: Build on the base class: own template, async search

Code

<!-- template: your own markup, bound to the inherited handlers -->
<input role="combobox" aria-expanded="true"
  aria-controls="people"
  aria-activedescendant={activeDescendant}
  oninput={handleInput}
  onkeydown={handleInputKeydown} />
<ul id="people" role="listbox"
  onmousedown={handleListboxMouseDown}>
  <template for:each={renderGroups} for:item="group">
    <template for:each={group.options} for:item="option">
      <li key={option.id} id={option.id} class={option.classes}
        role="option" data-option-id={option.id}
        onclick={handleOptionClick}
        onmousemove={handleOptionMouseMove}>
        {option.label}
      </li>
    </template>
  </template>
</ul>

// component
import FdSearchState from 'fandry/searchState';

export default class PeopleSearch extends FdSearchState {
  results = [];

  // what to list
  protected get source() { return this.results; }
  // the server already filtered
  protected filterItems(items) { return items; }
  // what picking one does
  protected commit(item) { this.picked = item.label; }

  // start the search
  protected setQuery(value) {
    super.setQuery(value);
    fetchPeople(value).then((people) => (this.results = people));
  }
}

// Keep the input and the options in the same template: aria-activedescendant
// can't point across a shadow boundary.
​