Bootstrap Search With Recent Searches — Free HTML CSS JS Snippet
Bootstrap Search With Recent Searches · Forms · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="container py-5 d-flex justify-content-center">
<div class="card bsrecent-card">
<div class="card-body p-3 position-relative">
<input type="search" class="form-control" id="bsrecentInput" placeholder="Search documentation..." autocomplete="off">
<div class="bsrecent-dropdown d-none" id="bsrecentDropdown">
<div class="d-flex justify-content-between align-items-center px-2 py-1">
<span class="small fw-semibold text-muted">Recent searches</span>
<button type="button" class="btn btn-link btn-sm p-0" id="bsrecentClear">Clear</button>
</div>
<ul class="list-unstyled mb-0" id="bsrecentList"></ul>
</div>
<p class="small text-muted mt-2 mb-0" id="bsrecentStatus"> </p>
</div>
</div>
</div>.bsrecent-card { width: 380px; max-width: 100%; border: 1px solid #eceef1; border-radius: 14px; }
.bsrecent-dropdown {
position: absolute; left: 12px; right: 12px; top: 58px; z-index: 5;
background: #fff; border: 1px solid #e5e7eb; border-radius: 10px;
box-shadow: 0 8px 20px rgba(20,22,28,.08); padding: 4px 0;
}
.bsrecent-item {
display: flex; align-items: center; gap: 8px; padding: 7px 12px;
font-size: 13.5px; cursor: pointer;
}
.bsrecent-item:hover { background: #f8f9fb; }const KEY = 'bsrecent-searches-demo';
const input = document.getElementById('bsrecentInput');
const dropdown = document.getElementById('bsrecentDropdown');
const list = document.getElementById('bsrecentList');
const status = document.getElementById('bsrecentStatus');
function readRecent() {
try { return JSON.parse(localStorage.getItem(KEY)) || []; } catch (e) { return []; }
}
function writeRecent(arr) {
try { localStorage.setItem(KEY, JSON.stringify(arr)); } catch (e) { /* storage unavailable — recent list just won't persist */ }
}
function renderDropdown() {
const recent = readRecent();
if (recent.length === 0) {
dropdown.classList.add('d-none');
return;
}
list.innerHTML = recent.map(q =>
'<li class="bsrecent-item" data-query="' + q.replace(/"/g, '"') + '">🕑 ' + q + '</li>'
).join('');
dropdown.classList.remove('d-none');
}
function addRecent(query) {
const trimmed = query.trim();
if (!trimmed) return;
let recent = readRecent().filter(q => q.toLowerCase() !== trimmed.toLowerCase());
recent.unshift(trimmed);
recent = recent.slice(0, 5);
writeRecent(recent);
}
function runSearch(query) {
addRecent(query);
status.textContent = 'Searching for "' + query + '"...';
input.blur();
}
input.addEventListener('focus', () => {
if (input.value.trim() === '') renderDropdown();
});
input.addEventListener('input', () => {
if (input.value.trim() === '') renderDropdown(); else dropdown.classList.add('d-none');
});
input.addEventListener('keydown', e => {
if (e.key === 'Enter' && input.value.trim()) runSearch(input.value);
});
list.addEventListener('click', e => {
const item = e.target.closest('.bsrecent-item');
if (!item) return;
input.value = item.dataset.query;
runSearch(item.dataset.query);
});
document.getElementById('bsrecentClear').addEventListener('click', e => {
e.stopPropagation();
writeRecent([]);
dropdown.classList.add('d-none');
});
document.addEventListener('click', e => {
if (!e.target.closest('.bsrecent-card')) dropdown.classList.add('d-none');
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Bootstrap Search With Recent Searches — HTML, CSS & JavaScript

The recent-searches list is real persisted state, not an in-memory array that resets on reload — readRecent()/writeRecent() wrap localStorage, both guarded with try/catch so a private browsing mode or a blocked storage API degrades to "no history" instead of throwing and breaking the search box entirely. addRecent() does the two things a recent-searches list actually needs to get right: it case-insensitively removes any existing entry matching the new query before unshifting it back to the front, so searching the same term twice moves it to the top rather than creating a duplicate, and it caps the list at 5 with slice(0, 5) so it can never grow without bound.
The dropdown only ever shows on an *empty* input focused — checked in both the focus and input listeners — which mirrors how recent-searches UI works almost everywhere: it's a shortcut for starting a new search, not a suggestion list competing with what's already been typed. A document-level click listener closes the dropdown the moment a click lands outside .bsrecent-card, the standard pattern for any popover that needs to dismiss on an outside click without needing a backdrop element.
Step by step
How to Use
- 1Load the snippetThe search box is empty with no dropdown showing yet — there's no history.
- 2Type a search and press EnterThe status line shows the search ran, and that query is now saved to recent history.
- 3Clear the input and focus it againA dropdown appears listing that search under "Recent searches".
- 4Run two more different searchesEach one is added to the top of the list, most recent first, up to 5 entries.
- 5Click a recent search from the dropdownIt fills the input and re-runs immediately, and moves back to the top of the list.
- 6Click "Clear"The whole recent-searches list empties, and the dropdown won't reappear until a new search is run.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Recent searches are inherently per-device, per-browser convenience state — localStorage is the right tool since it persists without needing an account or a network request just to remember what someone searched five minutes ago.
localStorage is sometimes blocked or cleared aggressively in private/incognito modes — the try/catch around readRecent/writeRecent means the search box still works perfectly, it simply won't remember anything between page loads.
Yes. Move the recent array into component state, initialize it by reading localStorage once in a mount hook, and write to localStorage inside the same addRecent-equivalent function that also updates state — the dedup-and-cap logic carries over unchanged.
This snippet models the recent-searches interaction itself; runSearch() is the single place to call your real search function or API with the query, in addition to (or instead of) the demo status message.