Skip to main content

How to Build a Reusable Multi-Select Lookup LWC in Salesforce

In plain words: A multi-select lookup is a search bar that allows users to search the Salesforce database and select multiple records at once (like tagging multiple Contacts on an Event). Because Salesforce does not provide a standard out-of-the-box component for this specific UI, developers must build a custom Lightning Web Component (LWC) using an input field, a dropdown list for search results, and an array to store the selected "tags."

Building forms in Salesforce often requires users to relate multiple records together. While standard lookup fields are great for a single 1-to-1 relationship, they fall short when you need to select multiple items.

In this guide, we will build a reusable custom multi-select lookup component. We will connect an LWC to an Apex controller to fetch live search results and manage the user's selections directly in the UI.

Step 1: Create the Apex Controller

Before the LWC can search for records, we need a backend Apex class to query the database. This class must use the @AuraEnabled(cacheable=true) annotation so the LWC can call it efficiently.

public with sharing class CustomLookupController {
    @AuraEnabled(cacheable=true)
    public static List<SObject> searchRecords(String searchTerm) {
        // Prevent empty queries from returning the whole database
        if (String.isBlank(searchTerm)) {
            return new List<SObject>();
        }
        
        // Example: Searching Accounts based on the user's text
        String searchKey = '%' + searchTerm + '%';
        return [SELECT Id, Name FROM Account WHERE Name LIKE :searchKey LIMIT 5];
    }
}

Step 2: Scaffold the LWC Component

Open your terminal (or VS Code) and use the Salesforce CLI to generate the component:

sf lightning generate component -n multiSelectLookup -d force-app/main/default/lwc

Step 3: Build the HTML Template

Open multiSelectLookup.html. Our UI needs three things: a search input, a dropdown list for the search results, and a visual list (like "pills" or tags) to show the user what they have already selected.

<template>
    <div class="lookup-container">
        
        <!-- Search Input -->
        <lightning-input type="search" label="Search Accounts" onchange={handleSearch}></lightning-input>
        
        <!-- Search Results Dropdown -->
        <template lwc:if={lookupResults.length}>
            <ul class="lookup-results">
                <template for:each={lookupResults} for:item="result">
                    <li key={result.Id} data-id={result.Id} data-name={result.Name} onclick={handleSelection}>
                        {result.Name}
                    </li>
                </template>
            </ul>
        </template>

        <!-- Selected Items (Pills) -->
        <ul class="selected-values">
            <template for:each={selectedValues} for:item="value">
                <li key={value.Id}>
                    {value.Name}
                </li>
            </template>
        </ul>

    </div>
</template>

Step 4: The JavaScript Logic

Open multiSelectLookup.js. We will import the Apex method and handle the user's input and clicks.

import { LightningElement, track } from 'lwc';
import searchRecords from '@salesforce/apex/CustomLookupController.searchRecords';

export default class MultiSelectLookup extends LightningElement {
    @track lookupResults = [];
    @track selectedValues = [];

    // Fires when the user types in the search bar
    handleSearch(event) {
        const searchTerm = event.target.value;
        
        // Call the Apex method
        searchRecords({ searchTerm: searchTerm })
            .then((result) => {
                this.lookupResults = result;
            })
            .catch((error) => {
                console.error('Error fetching lookup results:', error);
            });
    }

    // Fires when a user clicks a search result
    handleSelection(event) {
        const selectedId = event.target.dataset.id;
        const selectedName = event.target.dataset.name;
        
        const selectedItem = { Id: selectedId, Name: selectedName };
        
        // Add the clicked item to our selected array
        this.selectedValues = [...this.selectedValues, selectedItem];
        
        // Clear the search results so the dropdown closes
        this.lookupResults = [];
    }
}
Developer Trap: The Spread Operator
Notice how we used this.selectedValues = [...this.selectedValues, selectedItem]; instead of .push()? In LWC, using the spread operator (...) creates a completely new array. This is required to force the HTML template to "react" and re-render the screen. If you just use push(), the screen might not update!

Step 5: Add the CSS Styling

Open multiSelectLookup.css. We need to style the dropdown so it floats below the input field, just like a standard Salesforce lookup.

.lookup-container {
    position: relative;
    display: block;
    width: 100%;
}

/* Floating Dropdown styling */
.lookup-results {
    list-style: none;
    padding: 0;
    margin: 0;
    position: absolute;
    width: 100%;
    max-height: 200px;
    overflow-y: auto;
    border: 1px solid #ccc;
    background-color: #fff;
    z-index: 999;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

.lookup-results li {
    padding: 8px 12px;
    cursor: pointer;
    transition: background-color 0.2s;
}

.lookup-results li:hover {
    background-color: #f3f3f3;
}

/* Selected Items styling */
.selected-values {
    list-style: none;
    padding: 10px;
    margin-top: 10px;
    border: 1px dashed #ccc;
    background-color: #f9f9f9;
    border-radius: 4px;
}

.selected-values li {
    display: inline-block;
    padding: 4px 8px;
    margin: 4px;
    background-color: #0070d2;
    color: white;
    border-radius: 16px;
    font-size: 0.85rem;
}
360 Card: Making this Component Reusable
To make this a truly reusable tool for your entire org, consider adding these enhancements later:
  • Add an @api objectApiName property so you can pass in the target object from the parent component instead of hardcoding "Account" in Apex.
  • Add a "Remove" button (an X icon) next to the selected pills so users can un-select a record if they make a mistake.
  • Use JavaScript debouncing on the handleSearch method so it doesn't call Apex on every single keystroke.

Conclusion

By combining a lightning-input, a custom dropdown list, and an Apex search controller, you have successfully built a custom multi-select lookup component. This pattern is foundational for LWC development, teaching you how to manage user state, fetch server-side data, and manipulate CSS for modern UI design.

Happy coding!