How to Build an AJAX Live Search and Filter Table with PHP and MySQL

Build This Feature AJAX Live Search PHP + MySQL Vanilla JS No Page Reload 2026
Build This Feature — PHP Tutorial Series

How to Build an AJAX Live Search and Filter Table with PHP and MySQL — 2026

A live search box that filters table rows as you type — without reloading the page — transforms any management system from a clunky form-submit interface into something that feels modern and professional. This tutorial builds it from scratch using vanilla JavaScript fetch(), PHP, and MySQL — no jQuery, no framework, no complexity.

⚡ No page reload 🔒 SQL injection safe 📱 Works on mobile No jQuery required

The customer list in your management system currently shows all records and requires the user to scroll through hundreds of rows to find one person. A live search box lets them type “John” and immediately see only John-related records — faster, cleaner, and far more professional.

This tutorial builds the feature in two files: a PHP search endpoint that returns JSON, and a small vanilla JavaScript block that calls it on every keystroke. You can add this to any existing PHP table in under an hour.

Try It — Interactive Live Search Demo

Type in the box below to see exactly how the feature will behave on your project:

⚡ Live Preview — type to filter the customer table

IDNameEmailPlanStatus
Showing 8 of 8 records

How AJAX Live Search Works

⌨️
User types
in search box
🔁
JavaScript
sends fetch() request
🐘
PHP + MySQL
LIKE query + JSON
📊
Table updates
no page reload

Every time the user types a character, JavaScript sends a small HTTP request to your PHP file with the search term. PHP runs a LIKE query against the database and returns the matching rows as JSON. JavaScript then rebuilds the table HTML with the new results — all in under 100 milliseconds, without any page refresh.

1PHP Search Endpoint
2HTML Search Form
3JavaScript (Vanilla)
4Multi-column Search
5Dropdown Filter
6Add to Existing Page

Step 1 — The PHP Search Endpoint

Create ajax/search_customers.php. This file handles search requests from the browser. It receives a search term, queries the database safely using a prepared statement, and returns JSON.

PHP ajax/search_customers.php — the backend search handler

<?php
require_once '../config/dbconnection.php';

// Always return JSON — set header before anything else
header('Content-Type: application/json; charset=UTF-8');

// Get and sanitise the search term from the GET request
$search = trim($_GET['q'] ?? '');

// If search term is empty, return all records (or limit to first 50)
if ($search === '') {
    $result = mysqli_query($conn,
        "SELECT c.id, c.name, c.email, c.phone, c.status,
                p.plan_name, p.price
         FROM customers c
         JOIN internet_plans p ON c.plan_id = p.id
         ORDER BY c.name ASC LIMIT 50");
} else {
    // Prepared statement with LIKE wildcards on both sides
    // Searches name, email, and plan_name columns
    $pattern = '%' . $search . '%';
    $stmt = mysqli_prepare($conn,
        "SELECT c.id, c.name, c.email, c.phone, c.status,
                p.plan_name, p.price
         FROM customers c
         JOIN internet_plans p ON c.plan_id = p.id
         WHERE c.name  LIKE ?
            OR c.email LIKE ?
            OR p.plan_name LIKE ?
         ORDER BY c.name ASC
         LIMIT 100");
    // 'sss' = three string parameters, all the same search pattern
    mysqli_stmt_bind_param($stmt, 'sss', $pattern, $pattern, $pattern);
    mysqli_stmt_execute($stmt);
    $result = mysqli_stmt_get_result($stmt);
}

$customers = [];
while ($row = mysqli_fetch_assoc($result)) {
    $customers[] = $row;
}

// Return results as JSON — JavaScript reads this
echo json_encode([
    'results' => $customers,
    'count'   => count($customers),
    'query'   => $search
]);
💡 Why no string concatenation in the query? The old (insecure) approach was: $sql = "... WHERE name LIKE '%" . $search . "%'". If $search contains SQL code (e.g. ' OR '1'='1), it modifies your query — this is SQL injection. The prepared statement approach binds the search term as a data parameter, so it is always treated as a string value, never as SQL code. The LIKE wildcards (%) are added to the $pattern variable in PHP, not in the SQL string — this is the correct and safe approach.

Step 2 — HTML Search Form on Your Existing Page

HTML Add this above your existing customer table

<!-- Search + results count row -->
<div class="row mb-3">
  <div class="col-md-6">
    <div class="input-group">
      <span class="input-group-text">🔍</span>
      <input type="text"
             id="searchInput"
             class="form-control"
             placeholder="Search by name, email, or plan..."
             oninput="searchCustomers(this.value)"
             autocomplete="off">
      <button class="btn btn-outline-secondary"
              onclick="clearSearch()">✕ Clear</button>
    </div>
  </div>
  <div class="col-md-6 d-flex align-items-center justify-content-end">
    <span id="resultCount" class="text-muted small">Loading...</span>
  </div>
</div>

<!-- Loading indicator (hidden by default) -->
<div id="loadingIndicator" style="display:none" class="text-center py-3">
  <div class="spinner-border spinner-border-sm text-primary"></div>
  <span class="ms-2 text-muted small">Searching...</span>
</div>

<!-- The table — JavaScript replaces the tbody content -->
<div class="table-responsive">
<table class="table table-striped table-hover">
  <thead class="table-primary">
    <tr>
      <th>#</th>
      <th>Customer Name</th>
      <th>Email</th>
      <th>Phone</th>
      <th>Plan</th>
      <th>Price</th>
      <th>Status</th>
      <th>Actions</th>
    </tr>
  </thead>
  <tbody id="customerTableBody">
    <!-- PHP renders initial data here (see Step 6 for full page setup) -->
  </tbody>
</table>
</div>

Step 3 — The JavaScript (Vanilla, No jQuery)

JavaScript Add in a <script> tag at the bottom of your page

<script>
// Debounce: wait 300ms after user stops typing before sending request
// This prevents sending a request on every single keystroke
let searchTimer = null;

function searchCustomers(query) {
    clearTimeout(searchTimer);
    searchTimer = setTimeout(function() {
        performSearch(query);
    }, 300); // 300ms delay — feels instant but saves server requests
}

function performSearch(query) {
    const tbody  = document.getElementById('customerTableBody');
    const count  = document.getElementById('resultCount');
    const loader = document.getElementById('loadingIndicator');

    // Show loading indicator
    loader.style.display = 'block';

    // Fetch results from PHP endpoint
    fetch('/ajax/search_customers.php?q=' + encodeURIComponent(query))
        .then(res => res.json())
        .then(data => {
            loader.style.display = 'none';
            count.textContent = data.count + ' record' + (data.count !== 1 ? 's' : '') + ' found';
            tbody.innerHTML = buildTableRows(data.results, query);
        })
        .catch(err => {
            loader.style.display = 'none';
            count.textContent = 'Search error — try again';
            console.error('Search failed:', err);
        });
}

function buildTableRows(customers, query) {
    if (customers.length === 0) {
        return '<tr><td colspan="8" class="text-center text-muted py-4">' +
               'No records found matching "' + escapeHtml(query) + '"</td></tr>';
    }
    return customers.map(c => `
        <tr>
            <td>${c.id}</td>
            <td>${highlightTerm(escapeHtml(c.name), query)}</td>
            <td>${highlightTerm(escapeHtml(c.email), query)}</td>
            <td>${escapeHtml(c.phone || '—')}</td>
            <td>${highlightTerm(escapeHtml(c.plan_name), query)}</td>
            <td>£${parseFloat(c.price).toFixed(2)}</td>
            <td><span class="badge bg-${c.status === 'active' ? 'success' : 'secondary'}">
                ${escapeHtml(c.status)}</span></td>
            <td>
                <a href="/admin/view_customer.php?id=${c.id}" class="btn btn-sm btn-outline-primary">View</a>
                <a href="/admin/edit_customer.php?id=${c.id}" class="btn btn-sm btn-outline-secondary">Edit</a>
            </td>
        </tr>`).join('');
}

// Highlight the search term in yellow within cell text
function highlightTerm(text, term) {
    if (!term) return text;
    const regex = new RegExp('(' + escapeRegex(term) + ')', 'gi');
    return text.replace(regex, '<mark>$1</mark>');
}

function clearSearch() {
    document.getElementById('searchInput').value = '';
    performSearch('');
}

// Safely escape HTML to prevent XSS
function escapeHtml(str) {
    return String(str).replace(/&/g, '&amp;')
        .replace(/</g, '&lt;').replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;');
}

function escapeRegex(str) {
    return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Load all records when page first loads
document.addEventListener('DOMContentLoaded', function() {
    performSearch('');
});
</script>

Step 4 — Advanced: Dropdown Status Filter + Search Combined

Add a status dropdown that works alongside the search box — users can filter by “Active” customers named “John” at the same time.

HTML Add status dropdown filter next to search input

<select id="statusFilter" class="form-select"
        style="max-width:140px"
        onchange="searchCustomers(document.getElementById('searchInput').value)">
  <option value="">All Statuses</option>
  <option value="active">Active</option>
  <option value="inactive">Inactive</option>
  <option value="suspended">Suspended</option>
</select>

PHP Update search_customers.php to handle the status parameter

// Add to ajax/search_customers.php — handles optional status filter
$search = trim($_GET['q']      ?? '');
$status = trim($_GET['status'] ?? '');

$where = [];
$params = [];
$types = '';

if ($search !== '') {
    $pattern = '%' . $search . '%';
    $where[]  = '(c.name LIKE ? OR c.email LIKE ? OR p.plan_name LIKE ?)';
    $params   = [$pattern, $pattern, $pattern];
    $types   .= 'sss';
}

if ($status !== '') {
    $where[]  = 'c.status = ?';
    $params[] = $status;
    $types   .= 's';
}

$sql = "SELECT c.id, c.name, c.email, c.phone, c.status, p.plan_name, p.price
        FROM customers c
        JOIN internet_plans p ON c.plan_id = p.id";
if ($where) {
    $sql .= ' WHERE ' . implode(' AND ', $where);
}
$sql .= ' ORDER BY c.name ASC LIMIT 100';

$stmt = mysqli_prepare($conn, $sql);
if ($params) {
    mysqli_stmt_bind_param($stmt, $types, ...$params);
}
mysqli_stmt_execute($stmt);
$result = mysqli_stmt_get_result($stmt);

JavaScript Update fetch() call to include the status filter

function performSearch(query) {
    const status = document.getElementById('statusFilter').value;
    const url = '/ajax/search_customers.php'
              + '?q='      + encodeURIComponent(query)
              + '&status=' + encodeURIComponent(status);
    fetch(url).then(res => res.json()).then(data => { /* same as before */ });
}

4 Variants — Apply This Pattern to Any Table

🏥
Hospital — Patient Search
Search by patient name, ID, or doctor. Add a date range filter for appointment search. Change the SQL to query patients + doctors tables.
🎓
School — Student Search
Search student name or registration number. Add dropdown for class or year group. Works with the Student Management System from Codezips.
📦
Inventory — Product Search
Real-time product search by name or SKU. Add a “Low stock only” checkbox filter. Highlight items where quantity = 0 in red in the JavaScript row builder.
💊
Pharmacy — Medicine Lookup
Search medicine name, supplier, or expiry date range. This is a particularly strong project feature to demonstrate in portfolio interviews.

Frequently Asked Questions

Should I use GET or POST for the AJAX search request?

GET is correct for search requests. Searches are “safe” operations that read data without changing anything — GET is semantically correct for this. POST is reserved for operations that create or modify data. A secondary practical reason: GET requests can be bookmarked, cached, and debugged directly in the browser URL bar (navigate to /ajax/search_customers.php?q=john to test your endpoint directly).

My search is slow on tables with thousands of records. How do I speed it up?

Add a MySQL index on the columns you search most frequently: ALTER TABLE customers ADD INDEX idx_name (name); and ADD INDEX idx_email (email);. For LIKE searches with % at the start (e.g. LIKE '%john%'), MySQL cannot use a standard index for the leading wildcard. If you need very fast full-text search across large tables, switch to MySQL’s FULLTEXT index type and use MATCH ... AGAINST syntax instead of LIKE — this is significantly faster on large datasets.

What is debouncing and why does the code use it?

Debouncing is a technique that delays the execution of a function until a specified time has passed since the last time it was called. In this context, without debouncing, typing “John” would send 4 HTTP requests (J, Jo, Joh, John) in rapid succession. With 300ms debouncing, it only sends one request — for “John” — because the function waits 300ms after the last keystroke before firing. This reduces server load by up to 80% during fast typing while still feeling instant to the user.

How do I preserve pagination alongside live search?

Include pagination parameters in your AJAX request: add a page parameter and update the PHP to use LIMIT/OFFSET accordingly. The JavaScript needs to track the current page, reset to page 1 when the search term changes, and rebuild pagination controls from the total count returned by the PHP endpoint. This is more complex but follows the same pattern — the PHP returns both the results array and a total_count value, and the JavaScript builds page buttons from that count.


How to Export Data to Excel/CSV with PHP →

Export the filtered search results as Excel

How to Build a REST API in PHP →

The AJAX endpoint pattern is the foundation of REST APIs

ISP Management System →

Add this live search to your customer management page

How to Use AI to Debug PHP →

Debug AJAX errors with the right prompts

Last updated April 2026. Tested on PHP 8.2 / MySQL 8.0. Vanilla JavaScript fetch() API used — no jQuery dependency. SQL injection prevention via MySQLi prepared statements.

Leave a Comment

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

Scroll to Top