Skip to main content

Real-Time Search Table in Salesforce LWC: Reactive Filtering & SLDS Guide

In plain words: A Real-Time Search Table in LWC dynamically filters and displays records as the user types into an input field. By leveraging reactive JavaScript getters and native array filtering (Array.prototype.filter()), the table recalculates and renders matching rows instantly without requiring repeated roundtrips to the Salesforce database.

Providing instant record lookups inside custom dashboards, modal pickers, and console workspaces significantly improves user productivity. While server-side SOQL filtering is necessary for massive datasets, client-side filtering offers instantaneous, zero-latency feedback for datasets loaded into component memory. Below is a complete guide to building an accessible, responsive search table using standard Salesforce Lightning Design System (SLDS) styling.

1. How Client-Side Reactive Search Works in LWC

The client-side search pattern relies on three coordinated pieces inside the component lifecycle:

  • Master Dataset (data): Holds the immutable, original collection of records retrieved from an Apex wire adapter or mock store.
  • Search Query State (searchTerm): Tracks the live input string captured from the onchange event on the search box.
  • Computed Getter (filteredData): Automatically runs whenever searchTerm changes, performing case-insensitive string matching across multiple object fields and returning only matching records.
360 Search Table Architecture Card:
  • Reactivity Model: Evaluated via a JavaScript getter (get filteredData()).
  • Search Scope: Multi-field matching across Name, Email, and custom identifiers using String.toLowerCase().includes().
  • UI Framework: Native SLDS Data Table utilities (slds-table, slds-table_bordered).
  • Performance Threshold: Best suited for datasets under 1,000 records in memory.

2. Step-by-Step Implementation

Step 1: Component Markup (searchTable.html)
Combine a search input with an SLDS-styled data table, including empty-state messaging.
<template>
    <lightning-card title="Directory Search" icon-name="standard:contact">
        <div class="slds-p-around_medium">
            <!-- Search Bar Input -->
            <div class="slds-m-bottom_medium">
                <lightning-input
                    type="search"
                    label="Search Directory"
                    placeholder="Search by name or email..."
                    value={searchTerm}
                    onchange={handleSearch}>
                </lightning-input>
            </div>

            <!-- Responsive Table Container -->
            <div class="slds-scrollable_x">
                <table class="slds-table slds-table_cell-buffer slds-table_bordered slds-table_striped">
                    <thead>
                        <tr class="slds-line-height_reset">
                            <th scope="col">
                                <div class="slds-truncate" title="Name">Name</div>
                            </th>
                            <th scope="col">
                                <div class="slds-truncate" title="Email">Email</div>
                            </th>
                            <th scope="col">
                                <div class="slds-truncate" title="Role">Role</div>
                            </th>
                        </tr>
                    </thead>
                    <tbody>
                        <template for:each={filteredData} for:item="record">
                            <tr key={record.Id}>
                                <td data-label="Name">
                                    <div class="slds-truncate font-bold">{record.Name}</div>
                                </td>
                                <td data-label="Email">
                                    <div class="slds-truncate">{record.Email}</div>
                                </td>
                                <td data-label="Role">
                                    <div class="slds-truncate">{record.Role}</div>
                                </td>
                            </tr>
                        </template>
                    </tbody>
                </table>
            </div>

            <!-- Empty State Feedback -->
            <template lwc:if={isResultEmpty}>
                <div class="slds-text-align_center slds-p-around_large slds-text-color_weak">
                    No records found matching "{searchTerm}"
                </div>
            </template>
        </div>
    </lightning-card>
</template>
Step 2: JavaScript Controller (searchTable.js)
Implement the reactive filtering logic and normalize search queries for case-insensitive checks.
import { LightningElement, track } from 'lwc';

export default class SearchTable extends LightningElement {
    searchTerm = '';

    @track masterData = [
        { Id: 'REC-001', Name: 'John Doe', Email: 'john.doe@example.com', Role: 'Technical Architect' },
        { Id: 'REC-002', Name: 'Jane Smith', Email: 'jane.smith@example.com', Role: 'System Administrator' },
        { Id: 'REC-003', Name: 'Alex Johnson', Email: 'alex.johnson@example.com', Role: 'Platform Developer' },
        { Id: 'REC-004', Name: 'Sara Connor', Email: 'sara.connor@example.com', Role: 'Release Engineer' }
    ];

    handleSearch(event) {
        this.searchTerm = event.target.value;
    }

    get filteredData() {
        if (!this.searchTerm || this.searchTerm.trim() === '') {
            return this.masterData;
        }

        const query = this.searchTerm.trim().toLowerCase();

        return this.masterData.filter(record => {
            const nameMatch = record.Name ? record.Name.toLowerCase().includes(query) : false;
            const emailMatch = record.Email ? record.Email.toLowerCase().includes(query) : false;
            const roleMatch = record.Role ? record.Role.toLowerCase().includes(query) : false;

            return nameMatch || emailMatch || roleMatch;
        });
    }

    get isResultEmpty() {
        return this.filteredData.length === 0;
    }
}
Step 3: Component Stylesheet (searchTable.css)
.font-bold {
    font-weight: 600;
}

/* Ensure horizontal scrollability on narrow mobile viewports */
.slds-scrollable_x {
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
}

3. Common Traps & Performance Best Practices

Architecture Trap: Mutating the Master Dataset During Filter Operations
Writing statements like this.data = this.data.filter(...) permanently overwrites your original dataset. Once a user backspaces or clears their search query, the original records are lost forever. Always maintain an untouched master collection and return filtered rows through a separate computed getter or secondary display array.
Core Rule: Keep original dataset arrays immutable, sanitize searches with .trim().toLowerCase(), and rely on computed getters to drive real-time table rendering.
  • Defensive Null Checks: Always verify that properties exist before calling .toLowerCase() to prevent fatal Cannot read properties of undefined runtime exceptions.
  • Use Native SLDS Utility Classes: Replace manual CSS margin and padding declarations with standard classes like slds-p-around_medium and slds-m-bottom_medium for consistent theme alignment.
  • Client vs. Server Filtering Strategy: Use client-side getters for lists under 1,000 records. For larger enterprise tables, debounce input keystrokes and execute parameterized SOQL or SOSL queries on the server.

Summary

Building a search table with reactive client-side filtering delivers a fast, responsive user experience in Salesforce Lightning Web Components. By separating raw data from computed getters, enforcing case-insensitive checks, and styling tables with standard SLDS classes, developers can create polished directory views and interactive search utilities that scale cleanly.