Reader Stacks

Autocomplete Search in Laravel: Select2, jQuery UI, and Typeahead.js

Every autocomplete library here follows the identical shape underneath — a JS widget calling a Laravel endpoint that returns a filtered JSON array — the library choice only changes the front-end configuration syntax.

Three popular front-end libraries for autocomplete search — Select2, jQuery UI's Autocomplete widget, and Typeahead.js — all follow the same underlying pattern with Laravel: a JS widget calls a Laravel endpoint, which returns a filtered JSON array based on the search term.

The shared Laravel endpoint (works for all three libraries)

Route::get('/products/search', function (Request $request) {
    return Product::where('name', 'like', '%'.$request->q.'%')
        ->limit(10)
        ->get(['id', 'name']);
});

Select2

$('#product-select').select2({
    ajax: {
        url: '/products/search',
        dataType: 'json',
        delay: 250,
        data: function (params) {
            return { q: params.term };
        },
        processResults: function (data) {
            return {
                results: data.map(item => ({ id: item.id, text: item.name }))
            };
        }
    }
});

Select2's processResults callback is required to reshape the response into the specific {id, text} format Select2 expects internally — the raw endpoint response and Select2's expected shape rarely match exactly without this transformation step.

jQuery UI Autocomplete

$('#product-input').autocomplete({
    source: function (request, response) {
        $.get('/products/search', { q: request.term }, function (data) {
            response(data.map(item => ({ label: item.name, value: item.name, id: item.id })));
        });
    },
    minLength: 2,
    select: function (event, ui) {
        $('#product-id').val(ui.item.id);
    }
});

jQuery UI expects each result as an object with label (displayed text) and value (what fills the input on selection) — the select callback is where the actual selected item's ID gets captured into a hidden field, since the visible input itself only shows the product name, not its ID.

Typeahead.js (with Bloodhound as its suggestion engine)

const products = new Bloodhound({
    datumTokenizer: Bloodhound.tokenizers.whitespace,
    queryTokenizer: Bloodhound.tokenizers.whitespace,
    remote: {
        url: '/products/search?q=%QUERY',
        wildcard: '%QUERY'
    }
});

$('#product-typeahead').typeahead(null, {
    name: 'products',
    display: 'name',
    source: products
});

Typeahead.js's architecture separates the suggestion engine (Bloodhound, handling caching and the actual remote request) from the input UI itself — the %QUERY placeholder in the URL is replaced with the user's actual typed input at request time.

Debouncing the request across all three libraries

Select2's delay option and a manually implemented debounce for jQuery UI or Typeahead all serve the same purpose — avoiding firing a new request on every single keystroke, instead waiting for a brief pause in typing before actually querying the server, reducing unnecessary load for both the client and server.

Limiting results server-side, not just client-side

The limit(10) in the shared Laravel endpoint above matters regardless of which front-end library is used — without it, a broad search term could return every matching row in the table, which is both wasteful to transmit and unhelpful in a dropdown that can only usefully display a handful of suggestions at once.

Choosing between the three libraries

Select2 is the most full-featured (supporting multi-select, tagging, and rich styling out of the box) and the most commonly reached for in a Laravel/Bootstrap-based admin panel; jQuery UI's Autocomplete is lighter weight, useful when jQuery UI is already a project dependency for other components; Typeahead.js suits a case wanting more control over caching and suggestion ranking via Bloodhound's configuration.