Fandry UI
Salesforce

Lookup

Find and pick a record — one by default, or several with multiple. It fetches nothing itself: it reports what was typed, you run the query and hand the records back.

fandry-lookup

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 in the empty search box.
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.
results{ id, label, description?, disabled?, … }[][]Matches for the current search, shown exactly as given (never re-filtered). Extra fields ride along and come back in change.
loadingbooleanfalseShows a searching indicator while your query is in flight.
multiplebooleanfalseAllow any number of records, shown as removable chips with a “Clear all”. The list stays open after each pick, already-chosen records are not offered again, and Backspace in an empty field removes the last chip.
debounce-msnumber250Milliseconds typing must pause before search fires; 0 searches on every keystroke. Opening the list still searches at once.
valuestring | string[]''The selected id (an array of ids with multiple). Set it to render existing data; it updates when the user picks or clears. The lookup names each id from record/records or results; for any it can't, it fires resolve and shows the raw id (muted) until you supply the record.
record{ id, label, … } | nullnullSingle mode: the selected record, shown as the field's value with a clear button. On its own it preselects; once value has been set it only supplies the name for that id.
records{ id, label, … }[][]Multiple mode: the selected records. On its own it preselects; once value has been set it only supplies names for those ids (any subset is fine).
requiredbooleanfalseMarks the field required (asterisk + aria-required).
disabledbooleanfalseDisables the field and the pill's clear button.
element-propsRecord<string, unknown>{}Spread onto the native input; keys the component controls are ignored with a warning.
search (event)CustomEvent<{ query }>—Fires when the list opens by click or ArrowDown (immediately, so an empty query can offer recent records), after typing pauses, and in multiple mode after each pick.
resolve (event)CustomEvent<{ values }>—Fires once for ids set through value that the lookup can't name yet. Look them up and set record/records. It does not repeat for an id already asked about.
change (event)CustomEvent<{ value, record }> | CustomEvent<{ values, records }>—Single mode: { value, record } on pick, and { value: '', record: null } on clear. Multiple mode: { values, records } on every pick, removal and Clear all.
empty (slot)slot'No records found'Replaces the message shown when a search has no matches.
clear-all · searching (slots)slot'Clear all' · 'Searching…'Replace the Clear all button's text and the searching line.
messages{ searching, clear(name), remove(name) }{}Replaces the accessible names of the spinner and the clear and remove buttons, e.g. to translate them.
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.
selectionThe chosen record, in single-select mode.
selection-labelThe chosen record's name.
clear-buttonThe button that clears the chosen record.
chipsThe row of chosen records and the input, in multi-select mode.
chipOne chosen record.
chip-labelA chosen record's name.
chip-removeThe button that removes one chosen record.
inputThe native <input>.
clear-allThe Clear all button.
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.
statusA status line (a page count, "Searching…").
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-lookup
    label="Account"
    placeholder="Search accounts"
    debounce-ms="300"
    results={results}
    loading={loading}
    record={account}
    onsearch={handleSearch}
    onchange={handleChange}
  ></fandry-lookup>
</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: 999px;
  --fd-border-focus: 160 84% 36%;
  --fd-ring-color: 160 84% 36%;
  --fd-bg-muted: 160 40% 94%;
}

fandry-lookup::part(control) {
  padding-inline: 0.75rem;
  background: hsl(160 40% 98%);
}

fandry-lookup::part(panel) {
  border-radius: 1rem;
  box-shadow: 0 12px 32px hsl(160 40% 20% / 0.18);
}

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

Code

Link to section Code
<!-- template -->
<fandry-lookup
  label="Account"
  placeholder="Search accounts"
  results={results}
  loading={loading}
  record={account}
  onsearch={handleSearch}
  onchange={handleChange}
></fandry-lookup>

// component
async handleSearch(event) {
  this.loading = true;
  // Apex, GraphQL, UI API -- whatever you already use:
  this.results = await findAccounts(event.detail.query);
  this.loading = false;
}

handleChange(event) {
  this.account = event.detail.record; // null when cleared
}

// Ignore a slow earlier answer landing after a newer one: keep a request
// counter and only apply the result of the latest.
​

Example: Multiple records

Link to section Example: Multiple records

Code

<!-- template: bind `records` instead of `record` -->
<fandry-lookup
  label="Accounts"
  multiple
  results={results}
  loading={loading}
  records={accounts}
  onsearch={handleSearch}
  onchange={handleChange}
></fandry-lookup>

// component
handleChange(event) {
  this.accounts = event.detail.records; // also event.detail.values (the ids)
}

// The list stays open after each pick and fires `search` again with an
// empty query, so refresh it (recent records, minus what's now chosen).
// Records already chosen are hidden from `results` automatically.
​

Example: Existing value (single and multiple)

Link to section Example: Existing value (single and multiple)

Code

<!-- template: bind `value`. Bind `record`/`records` too, to supply names. -->
<fandry-lookup
  label="Account"
  value={accountId}
  record={account}
  onresolve={handleResolve}
  onchange={handleChange}
></fandry-lookup>

<fandry-lookup
  label="Accounts"
  multiple
  value={accountIds}
  records={accounts}
  onresolve={handleResolveMany}
  onchange={handleChangeMany}
></fandry-lookup>

// component
accountId = '001C';               // straight off a saved record
accountIds = ['001A', '001D'];

// The lookup can't name an id it has no record for. It shows the id, and asks:
async handleResolve(event) {
  const [account] = await getAccounts(event.detail.values);
  this.account = account;
}

async handleResolveMany(event) {
  const found = await getAccounts(event.detail.values);
  this.accounts = [...this.accounts, ...found];
}

// Only the ids it asked about are in event.detail.values. Records for ids
// that aren't in `value` are ignored: value decides what's selected.
handleChange(event) {
  this.accountId = event.detail.value;
}
handleChangeMany(event) {
  this.accountIds = event.detail.values;
  this.accounts = event.detail.records;
}
​