Latest Update : 

Step-by-Step Guide to Creating a Custom Record Picker for LWR Sites

February 23, 2026
166 Views
Step-by-Step Guide to Creating a Custom Record Picker for LWR Sites
Summarize this blog post with:

Lightning Web RuntimeFlwr (LWR) is Salesforce’s modern runtime engine for building Experience Cloud sites. It is optimised for performance, strict security boundaries, and scalable external access. Because of architectural differences between LWR and standard Lightning Experience, several Lightning components are not supported in LWR.

One such component is lightning-record-picker.

This component depends on Lightning Experience–specific services such as Lightning Data Service (LDS), UI Record APIs, and implicit metadata resolution. These services are not exposed in LWR. As a result, lightning-record-picker cannot function in LWR sites.

To implement lookup functionality in LWR, a custom Lightning Web Component must be created.

This blog demonstrates how to build a configurable, reusable Record Picker component for LWR.

Custom Record Picker Overview

Since lightning-record-picker depends on Lightning Experience services (LDS, UI Record APIs, implicit metadata), it cannot run inside LWR. A custom lookup must handle data explicitly without relying on the internal runtime context.

This Record Picker is built specifically for LWR’s lightweight and service-restricted architecture.

What This Component Supports?
  • Dynamic record search using configurable fields
  • Primary and secondary display fields in dropdown results
  • Custom ID field mapping via idField
  • Search field configuration using searchFields
  • Initial result limit controlled by defaultCount
  • Keyboard and mouse selection handling
  • Clear (X) button for reset
  • Custom recordselect event returning selected record data

The component is fully data-driven,

  • Records are passed through the records property
  • Search behaviour is defined through configurable field mappings
  • UI rendering is independent of object metadata

This approach removes dependency on Lightning Experience services while preserving lookup functionality in LWR Experience Cloud sites.

recordPicker.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>65.0</apiVersion>
    <isExposed>false</isExposed>
    <masterLabel>Record Picker</masterLabel>
</LightningComponentBundle>

 

recordPicker.css

					/* --- Container --- */
.rp-container {
    position: relative;
    width: 100%;
    box-sizing: border-box;
    overflow: visible ! important;
    z-index: 99999;
    transform: translateZ(0);
    color: #304051;
    font-family: "Inter";
    font-size: 32px;
}

/* --- Input wrapper (holds input + icon) --- */
.rp-input-wrapper {
  position: relative;
  width: 100%;
  box-sizing: border-box;
}

/* --- Input --- */
.rp-input {
  width: 100%;
  padding: 10px 36px 10px 12px;
  border: var(--markley-border-color);
  border-radius: var(--markley-border-radius);
  background-color: #ffffff;
  font-size: 15px;
  color: #12202b;
  outline: none;
  -webkit-appearance: none;
  transition: box-shadow 120ms ease, border-color 120ms ease;
  box-sizing: border-box;
}

/* Input focus state */
.rp-input:focus {
  border-color: #5b7cff;
  box-shadow: 0 0 0 3px rgba(91, 124, 255, 0.08);
}

/* Placeholder color */
.rp-input::placeholder {
  color: #9aa4ae;
}

/* --- Icon (right inside the input) --- */
.rp-icon {
  position: absolute;
  right: 10px;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  height: 20px;
  width: 20px;
  cursor: pointer;
  user-select: none;
  font-size: 12px;
  color: #6b6b6b;
  transition: color 120ms ease, transform 120ms ease;
}

/* Larger clickable area on mobile */
.rp-icon::after {
  content: "";
  position: absolute;
  inset: -6px; /* expand hit area */
}

/* Hover / active */
.rp-icon:hover {
  color: #23374f;
}
.rp-icon:active {
  transform: translateY(-50%) scale(0.98);
}

/* If you place an SVG inside .rp-icon, ensure it is aligned */
.rp-icon svg {
  display: block;
  height: 14px;
  width: 14px;
}

/* --- Dropdown list --- */
.rp-list {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  width: 100%;
  max-height: 260px;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 6px 0;
  margin: 0;
  list-style: none;
  background: #ffffff;
  border: var(--markley-border-color);
  border-radius: 8px;
  box-shadow: 0 10px 30px rgba(18, 32, 43, 0.08);
  z-index:  99999 !important; /* high so it sits above page content */
  box-sizing: border-box;
}

/* If you have container with overflow hidden, this helps (but ideally ensure parent allows visible overflow) */
.rp-container { -webkit-transform: translateZ(0); }

/* --- List item --- */
.rp-item {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 8px 12px;
  cursor: pointer;
  min-height: 44px;
  box-sizing: border-box;
  border-radius: 6px;
  transition: background 120ms ease;
}

/* Hover/focus state */
.rp-item:hover,
.rp-item[aria-selected="true"] {
  background: #f3f6ff;
}

/* Title & subtitle */
.rp-title {
    font-family: 'Inter';
    font-weight: 600;
    font-size: 14px;
    color: #0b1b2b;
    line-height: 1.1;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

.rp-subtitle {
  font-size: 12px;
  color: #6b6b6b;
  margin-top: 3px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Divider between items (soft) */
.rp-item + .rp-item {
  margin-top: 4px;
}

/* Empty state */
.rp-empty {
  text-align: center;
  color: #8b99a6;
  padding: 12px;
  font-size: 13px;
}

/* --- "Show more" item style (optional) --- */
.rp-show-more {
  padding: 10px 12px;
  text-align: center;
  cursor: pointer;
  font-size: 13px;
  color: #2b63ff;
}

/* --- Scrollbar styling for webkit browsers --- */
.rp-list::-webkit-scrollbar {
  width: 10px;
}
.rp-list::-webkit-scrollbar-thumb {
  background: rgba(18, 32, 43, 0.08);
  border-radius: 8px;
}
.rp-list::-webkit-scrollbar-track {
  background: transparent;
}

/* --- Responsive / touch adjustments --- */
@media (hover: none) and (pointer: coarse) {
  .rp-item { min-height: 56px; padding: 12px 14px; }
  .rp-input { padding: 12px 44px 12px 12px; }
}

/* --- Small utility classes (optional) --- */
.hidden { display: none !important; }
.center { text-align: center; }

/* --- Accessibility focus ring for keyboard users on list items --- */
.rp-item:focus {
  outline: 3px solid rgba(91, 124, 255, 0.12);
  outline-offset: -3px;
  background: #eef3ff;
}

/* CLEAR X BUTTON */
.rp-clear {
    position: absolute;
    right: 30px;
    top: 50%;
    transform: translateY(-50%);
    cursor: pointer;
    font-size: 15px;
    color: black;
}
				
recordPicker.html

<template>
    <div class="rp-container">

        <div class="rp-input-wrapper">

            <input
                class="rp-input"
                type="text"
                placeholder={placeholder}
                value={searchKey}
                onclick={handleInputClick}
                onfocus={handleFocus}
                oninput={handleInput}
            />

            <!-- DROPDOWN ICON -->
            <span class="rp-icon" onclick={toggleDropdown}>▼</span>

            <!-- CLEAR X BUTTON -->
            <template if:true={searchKey}>
                <span class="rp-clear" onclick={clearSelection}>✕</span>
            </template>

        </div>

        <!-- DROPDOWN LIST -->
        <template if:true={showDropdown}>
            <ul class="rp-list">

                <template for:each={filtered} for:item="rec">
                    <li
                        key={rec.uniqueKey}
                        class="rp-item"
                        data-key={rec.uniqueKey}
                        onpointerdown={handleSelect}
                    >
                        <span class="rp-title">{rec.primary}</span>
                        <span class="rp-subtitle">{rec.secondary}</span>
                    </li>
                </template>

            </ul>
        </template>

    </div>
</template>
recordPicker.js

import { LightningElement, api, track } from 'lwc';

export default class RecordPicker extends LightningElement {
    @api records = [];
    @api primaryField;
    @api secondaryField;
    @api searchFields;
    @api idField;
    @api placeholder = "Search...";
    @api defaultCount = 20;
    @api minChars = 1;

    @api
    clear() {
        this.searchKey = "";
        this.showDropdown = false;
        this.filtered = [];
    }

    @track filtered = [];
    searchKey = "";
    showDropdown = false;

    connectedCallback() {
        document.addEventListener("mousedown", this.handleOutsideClick);
    }

    disconnectedCallback() {
        document.removeEventListener("mousedown", this.handleOutsideClick);
    }

    handleOutsideClick = (event) => {
        if (!this.showDropdown) return;
        if (!this.template.contains(event.target)) {
            this.showDropdown = false;
        }
    };

    clearSelection() {
        this.searchKey = "";
        this.showDefaultRecords();
    }

    toggleDropdown() {
        if (this.showDropdown) this.showDropdown = false;
        else this.showDefaultRecords();
    }

    handleInputClick() {
        if (!this.showDropdown) {
            this.showDefaultRecords();
        }
    }

    handleFocus() {
        this.showDefaultRecords();
    }

    handleInput(event) {
        this.searchKey = event.target.value;

        if (!this.searchKey) {
            this.showDefaultRecords();
            return;
        }
        this.filterConfiguredList();
    }

    showDefaultRecords() {
        this.filtered = this.records.slice(0, this.defaultCount).map((rec) => ({
            uniqueKey: rec[this.idField],
            primary: rec[this.primaryField],
            secondary: this.secondaryField ? rec[this.secondaryField] : "",
            original: rec
        }));

        this.showDropdown = true;
    }

    filterConfiguredList() {
        const key = this.searchKey.toLowerCase();
        const fields = this.searchFields.split(",").map((f) => f.trim());

        this.filtered = this.records
            .filter((rec) =>
                fields.some(
                    (f) => rec[f] && rec[f].toLowerCase().includes(key)
                )
            )
            .map((rec) => ({
                uniqueKey: rec[this.idField],
                primary: rec[this.primaryField],
                secondary: this.secondaryField ? rec[this.secondaryField] : "",
                original: rec
            }));

        this.showDropdown = true;
    }

    handleSelect(event) {
        const li = event.target.closest("li");
        const key = li.dataset.key;

        const selected = this.filtered.find((r) => r.uniqueKey == key);
        if (!selected) return;

        this.searchKey = selected.primary;
        this.showDropdown = false;

        this.dispatchEvent(
            new CustomEvent("recordselect", {
                detail: { key: key, record: selected.original }
            })
        );
    }
}

Apex Controller Example

Apex Class

public class ContactController {
    @AuraEnabled(cacheable=true)
    public static List<Contact> GetContacts(){
        return [
            SELECT Id, Name, Title, Email, MobilePhone, Fax, AccountId
            FROM Contact
        ];
    }
}

Example Usage in Another LWC

recordPickerTest.js

					import {
    LightningElement,
    wire
} from 'lwc';
import GetContacts from '@salesforce/apex/ContactController.GetContacts';

export default class RecordPickerTest extends LightningElement {
    contacts;
    @wire(GetContacts)
    wiredContacts({
        error,
        data
    }) {
        console.error('record selected', JSON.stringify(data));
        console.error('error', JSON.stringify(error));
        if (data) {
            this.contacts = data;
        } else if (error) {
            this.contacts = undefined;
        }
    }

    handleRecordSelect(e) {
        console.log('record selected', e.detail);
    }
}
				

Conclusion

LWR does not provide Lightning Experience–specific services required by lightning-record-picker. Custom lookup components must be implemented using explicit data handling and client-side filtering.

The solution above provides a reusable and configurable Record Picker that functions reliably in LWR Experience Cloud sites without relying on unsupported runtime services.

How useful was this post?

Click on a star to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Written by

Dev Anand

A dynamic engineer, innovative thinker, initiative taker and multi technology professional with exceptional logical, analytical and management skills possess a decade experience in Software Development and Salesforce CRM Solutioning. Enrich experience in converting business needs to Salesforce Experience. Worked on multiple RFPs and POCs. 50+ Integrations between Salesforce and other Platforms. Experience in LWC, Aura, Apex, JS, HTML, PHP, WordPress, Magento and many others.

Get the latest tips, news, updates, advice, inspiration, and more….

Contributor of the month
contributor
Garima Chaturvedi

Marketing lead | CDP lead | SFMC | Martech | Automation | Cert. SF Data cloud consultant

...
Categories
...
Boost Your Brand's Visibility

Want to promote your products/services in front of more customers?

...

Leave a Reply

Your email address will not be published. Required fields are marked *