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 }
})
);
}
}