Patterns Easy
Debounced Search
Search input pattern with debounce timing, loading state, and stale-request result protection.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
* {
box-sizing: border-box;
}
body {
margin: 0;
background: #070c17;
color: #e2e8f0;
font-family: "Sora", system-ui, sans-serif;
}
.shell {
width: min(700px, calc(100% - 2rem));
margin: 2rem auto;
}
label {
display: grid;
gap: 0.4rem;
color: #94a3b8;
font-size: 0.85rem;
}
input {
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
background: #10182b;
color: #e2e8f0;
padding: 0.6rem;
}
#state {
min-height: 1.2rem;
font-size: 0.85rem;
color: #93c5fd;
}
ul {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0.55rem;
}
li {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 10px;
background: #111c30;
padding: 0.65rem;
}(() => {
const DATA = [
"billing portal",
"dashboard metrics",
"error tracking",
"event ingestion",
"feature flags",
"git workflow",
"incident report",
"knowledge base",
"live updates",
"message queue",
"notification center",
"payment retries",
"query planner",
"release notes",
"search analytics",
"session replay",
"status page",
"team invites",
];
const input = document.getElementById("search");
const state = document.getElementById("state");
const results = document.getElementById("results");
let debounceId = 0;
let requestToken = 0;
const render = (items) => {
results.innerHTML = "";
for (const item of items) {
const li = document.createElement("li");
li.textContent = item;
results.appendChild(li);
}
};
const search = (query, token) =>
new Promise((resolve) => {
setTimeout(
() => {
const normalized = query.trim().toLowerCase();
const data = normalized
? DATA.filter((item) => item.includes(normalized)).slice(0, 8)
: DATA.slice(0, 8);
resolve({ token, data });
},
260 + Math.random() * 280
);
});
const run = async () => {
const query = input.value;
const token = ++requestToken;
state.textContent = "Searching...";
const response = await search(query, token);
if (response.token !== requestToken) return;
render(response.data);
state.textContent = response.data.length
? `${response.data.length} result${response.data.length === 1 ? "" : "s"}`
: "No results";
};
input.addEventListener("input", () => {
clearTimeout(debounceId);
debounceId = window.setTimeout(run, 300);
});
run();
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Debounced Search</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="shell">
<h1>Debounced Search</h1>
<label>
Search
<input id="search" type="search" placeholder="Type a keyword" autocomplete="off" />
</label>
<p id="state" role="status" aria-live="polite"></p>
<ul id="results"></ul>
</main>
<script src="script.js"></script>
</body>
</html>Debounced Search
A clean search pattern that reduces request churn and prevents out-of-order responses from overriding newer results.
Features
- 300ms input debounce
- Loading and empty states
- Request token stale-guard
- Fast keyboard interaction