All articles

ASP.NET Core Autocomplete Textbox: datalist, fetch and jQuery UI

Build an autocomplete textbox in ASP.NET Core with the native datalist element and fetch, a custom dropdown, or the jQuery UI widget, with debounced requests.

0 · log in to like, save & follow Share on LinkedIn Share on X

Autocomplete — suggestions appearing as the user types — used to require the jQuery UI widget. Today the pieces are built into the platform: an ASP.NET Core action returns matching items as JSON, and the browser's native <datalist> element (or a few lines of fetch code) renders the suggestions. This tutorial builds both variants, plus the debounce pattern that keeps your server from being hammered on every keystroke.

cover

The suggestions endpoint

public class CatalogController : Controller
{
    private static readonly string[] AllProducts =
    [
        "Galaxy A15 Mobile", "Pixel 9", "iPhone 16",
        "Air Pods", "Sony WH-1000XM5", "JBL Flip",
        "Pen drive", "SSD 1TB", "MicroSD 256GB",
    ];

    public JsonResult Suggest(string term) =>
        Json(AllProducts
            .Where(p => p.Contains(term ?? "", StringComparison.OrdinalIgnoreCase))
            .Take(8));
}

Verified: GET /catalog/suggest?term=ss → ["SSD 1TB"].

In production this is a database query — with EF Core, Where(p => p.Name.Contains(term)).Take(8) translates to SQL LIKE. Always cap the results (Take) — an autocomplete needs eight good suggestions, not eight thousand.

Variant 1 — native datalist (zero JavaScript frameworks)

<datalist> gives you the dropdown UI for free; you only fill it:

<input id="product-search" list="product-suggestions"
       placeholder="Search products…" autocomplete="off" />
<datalist id="product-suggestions"></datalist>

<script>
    const input = document.getElementById('product-search');
    const list = document.getElementById('product-suggestions');
    let debounceTimer;

    input.addEventListener('input', function () {
        clearTimeout(debounceTimer);
        const term = this.value.trim();
        if (term.length < 2) { list.innerHTML = ''; return; }

        debounceTimer = setTimeout(async () => {
            const response = await fetch(`/catalog/suggest?term=${encodeURIComponent(term)}`);
            if (!response.ok) return;
            const suggestions = await response.json();
            list.innerHTML = '';
            for (const s of suggestions)
                list.append(new Option(s));
        }, 250);
    });
</script>

Three details that matter:

  • Debounce — the setTimeout/clearTimeout pair waits 250 ms after the last keystroke before calling the server. Typing "galaxy" fires one request, not six.
  • Minimum length — skip lookups under two characters; one-letter matches are noise and load.
  • encodeURIComponent — the term goes into a URL; encode it so spaces and special characters survive.

This is the version to reach for first: no library, accessible out of the box, works on mobile.

Variant 2 — custom dropdown with fetch

When you need richer suggestions than <datalist> allows (images, secondary text, highlight-as-you-type), render your own list — same endpoint, same debounce:

<div class="ac-wrap">
    <input id="search" placeholder="Search products…" autocomplete="off" />
    <ul id="search-results" hidden></ul>
</div>

<script>
    const box = document.getElementById('search');
    const results = document.getElementById('search-results');
    let timer;

    box.addEventListener('input', function () {
        clearTimeout(timer);
        const term = this.value.trim();
        if (term.length < 2) { results.hidden = true; return; }

        timer = setTimeout(async () => {
            const items = await (await fetch(`/catalog/suggest?term=${encodeURIComponent(term)}`)).json();
            results.innerHTML = '';
            for (const item of items) {
                const li = document.createElement('li');
                li.textContent = item;
                li.addEventListener('click', () => {
                    box.value = item;
                    results.hidden = true;
                });
                results.append(li);
            }
            results.hidden = items.length === 0;
        }, 250);
    });

    document.addEventListener('click', e => {
        if (!e.target.closest('.ac-wrap')) results.hidden = true;
    });
</script>

Style #search-results as an absolutely-positioned list under the input. For a production-grade custom widget also handle arrow-key navigation and aria-* attributes — or use <datalist>, which does all of that for free.

The jQuery UI version (legacy)

If you maintain a page that already uses jQuery UI's autocomplete, it plugs into the same endpoint:

$('#product-search').autocomplete({
    source: function (request, response) {
        $.getJSON('/catalog/suggest', { term: request.term }, response);
    },
    minLength: 2,
    delay: 250
});

Nothing on the server changes between all three variants — one JSON endpoint serves them all, which is exactly the point of separating the API from the UI. For new work, skip the jQuery UI dependency and use the datalist.

Related

Frequently asked questions

How do I add autocomplete to a textbox in ASP.NET Core?

Expose a small endpoint that returns matching suggestions as JSON, then fill a native <datalist> from it with fetch as the user types. No plugin is needed.

Do I still need jQuery UI for autocomplete?

No. The native datalist element covers most cases, and a small custom dropdown handles the rest. jQuery UI is only worth keeping in pages that already use it.

How do I avoid a request on every keystroke?

Debounce the input: wait a short pause (around 250 ms) after the last keystroke before calling the endpoint, and cap the number of suggestions it returns.

Related reading

Enjoyed this article? Get the best GeeksArray articles in your inbox — once a week, no spam, unsubscribe anytime.

Comments (0)

Log in to join the conversation.

No comments yet — be the first to share your thoughts.