UI Components Medium
Live Search with Debounce
A real-time search interface that filters results as you type, optimized with debouncing to minimize performance overhead.
Open in Lab
MCP
vanilla-js css react tailwind svelte vue
Targets: TS JS HTML React Svelte Vue
Code
:root {
--ls-primary: #60a5fa;
--ls-primary-glow: rgba(96, 165, 250, 0.2);
--ls-bg: rgba(255, 255, 255, 0.04);
--ls-border: rgba(255, 255, 255, 0.1);
--ls-text: #f8fafc;
--ls-muted: #94a3b8;
--ls-hover: rgba(255, 255, 255, 0.04);
--ls-results-bg: rgba(15, 23, 42, 0.9);
}
.search-widget {
max-width: 500px;
margin: 0 auto;
font-family: "Inter", system-ui, sans-serif;
color: var(--ls-text);
}
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
background: var(--ls-bg);
border: 1px solid var(--ls-border);
border-radius: 14px;
padding: 0.25rem 1rem;
transition: all 0.2s ease;
backdrop-filter: blur(12px);
}
.search-input-wrapper:focus-within {
border-color: rgba(96, 165, 250, 0.5);
box-shadow: 0 0 0 4px var(--ls-primary-glow);
}
#live-search-input {
width: 100%;
border: none;
padding: 0.75rem 0.5rem;
font-size: 1rem;
outline: none;
background: transparent;
color: var(--ls-text);
}
#live-search-input::placeholder {
color: var(--ls-muted);
}
.search-icon {
font-size: 1.125rem;
opacity: 0.4;
}
.clear-btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
width: 24px;
height: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--ls-muted);
font-size: 1rem;
line-height: 1;
transition: all 0.2s;
}
.clear-btn:hover {
color: var(--ls-text);
background: rgba(255, 255, 255, 0.12);
}
.spinner {
width: 18px;
height: 18px;
border: 2px solid rgba(96, 165, 250, 0.2);
border-top-color: var(--ls-primary);
border-radius: 50%;
animation: ls-spin 0.6s linear infinite;
}
.search-results-container {
margin-top: 0.75rem;
background: var(--ls-results-bg);
border-radius: 14px;
border: 1px solid var(--ls-border);
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5);
overflow: hidden;
backdrop-filter: blur(20px);
}
.results-meta {
padding: 0.625rem 1rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--ls-muted);
background: rgba(255, 255, 255, 0.03);
border-bottom: 1px solid var(--ls-border);
}
.results-list {
list-style: none;
margin: 0;
padding: 0;
max-height: 360px;
overflow-y: auto;
}
.results-list::-webkit-scrollbar {
width: 4px;
}
.results-list::-webkit-scrollbar-track {
background: transparent;
}
.results-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.result-item {
padding: 0.875rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
cursor: pointer;
transition: background 0.15s;
}
.result-item:last-child {
border-bottom: none;
}
.result-item:hover {
background: var(--ls-hover);
}
.result-title {
display: block;
font-weight: 600;
font-size: 0.9375rem;
color: var(--ls-text);
margin-bottom: 2px;
}
.result-category {
font-size: 0.75rem;
color: var(--ls-primary);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.no-results {
padding: 2.5rem 1.5rem;
text-align: center;
color: var(--ls-muted);
}
@keyframes ls-spin {
to {
transform: rotate(360deg);
}
}// Sample Data
const data = [
{ title: "Accordion Spring", category: "UI Components" },
{ title: "Digital Clock", category: "Widgets" },
{ title: "Video Player", category: "Media" },
{ title: "Like Button", category: "Social" },
{ title: "Stock Ticker", category: "Real-time" },
{ title: "Quiz Widget", category: "Interactive" },
{ title: "Memory Card Game", category: "Games" },
{ title: "Currency Converter", category: "Utilities" },
];
const input = document.getElementById("live-search-input");
const resultsList = document.getElementById("search-results-list");
const resultsCount = document.getElementById("results-count");
const clearBtn = document.getElementById("clear-search");
const spinner = document.getElementById("search-spinner");
const noResults = document.getElementById("no-results");
const queryDisplay = document.getElementById("search-query-display");
// Debounce Function
function debounce(fn, delay) {
let timeoutId;
return function (...args) {
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
function performSearch(query) {
spinner.style.display = "none";
if (!query) {
resultsList.innerHTML = "";
resultsCount.textContent = "Enter a search term...";
clearBtn.style.display = "none";
noResults.style.display = "none";
return;
}
clearBtn.style.display = "block";
const filtered = data.filter(
(item) =>
item.title.toLowerCase().includes(query.toLowerCase()) ||
item.category.toLowerCase().includes(query.toLowerCase())
);
displayResults(filtered, query);
}
function displayResults(results, query) {
resultsList.innerHTML = "";
if (results.length === 0) {
noResults.style.display = "block";
queryDisplay.textContent = query;
resultsCount.textContent = "0 Results Found";
return;
}
noResults.style.display = "none";
resultsCount.textContent = `${results.length} Results Found`;
results.forEach((item) => {
const li = document.createElement("li");
li.className = "result-item";
li.innerHTML = `
<span class="result-category">${item.category}</span>
<span class="result-title">${item.title}</span>
`;
resultsList.appendChild(li);
});
}
// Event Listeners
const debouncedSearch = debounce((e) => {
performSearch(e.target.value.trim());
}, 300);
input.addEventListener("input", (e) => {
if (e.target.value.trim()) {
spinner.style.display = "block";
}
debouncedSearch(e);
});
clearBtn.addEventListener("click", () => {
input.value = "";
performSearch("");
input.focus();
});
// Initial state
performSearch("");<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Live Search</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="search-widget">
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input type="text" id="live-search-input" placeholder="Search components, docs..." autocomplete="off" />
<button id="clear-search" class="clear-btn" style="display: none;">×</button>
<div id="search-spinner" class="spinner" style="display: none;"></div>
</div>
<div class="search-results-container">
<div id="results-count" class="results-meta"></div>
<ul id="search-results-list" class="results-list">
<!-- Search results will appear here -->
</ul>
<div id="no-results" class="no-results" style="display: none;">
<p>No matches found for "<span id="search-query-display"></span>"</p>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>import { useState, useEffect, useCallback } from "react";
const ITEMS = [
{ id: 1, title: "Command Palette", category: "UI Component", tags: ["keyboard", "search"] },
{ id: 2, title: "Animated Tabs", category: "UI Component", tags: ["tabs", "animation"] },
{ id: 3, title: "Drag & Drop List", category: "UI Component", tags: ["drag", "sortable"] },
{ id: 4, title: "CSS Typewriter", category: "Animation", tags: ["text", "css"] },
{ id: 5, title: "SVG Path Drawing", category: "Animation", tags: ["svg", "gsap"] },
{ id: 6, title: "Mouse Trail", category: "Animation", tags: ["canvas", "mouse"] },
{ id: 7, title: "Kanban Board", category: "SaaS", tags: ["drag", "project"] },
{ id: 8, title: "Calendar View", category: "SaaS", tags: ["dates", "scheduler"] },
{ id: 9, title: "Data Table", category: "UI Component", tags: ["table", "sort"] },
{ id: 10, title: "Audio Player", category: "Media", tags: ["audio", "html5"] },
{ id: 11, title: "Video Player", category: "Media", tags: ["video", "html5"] },
{ id: 12, title: "Quiz Widget", category: "Interactive", tags: ["quiz", "learning"] },
];
const CATEGORY_COLORS: Record<string, string> = {
"UI Component": "#58a6ff",
Animation: "#bc8cff",
SaaS: "#7ee787",
Media: "#f1e05a",
Interactive: "#ff7b72",
};
export default function LiveSearchRC() {
const [query, setQuery] = useState("");
const [results, setResults] = useState(ITEMS);
const [loading, setLoading] = useState(false);
const [focused, setFocused] = useState(false);
const search = useCallback((q: string) => {
const lower = q.toLowerCase().trim();
return ITEMS.filter(
(item) =>
!lower ||
item.title.toLowerCase().includes(lower) ||
item.category.toLowerCase().includes(lower) ||
item.tags.some((t) => t.includes(lower))
);
}, []);
useEffect(() => {
if (!query) {
setResults(ITEMS);
setLoading(false);
return;
}
setLoading(true);
const id = setTimeout(() => {
setResults(search(query));
setLoading(false);
}, 300);
return () => clearTimeout(id);
}, [query, search]);
return (
<div className="min-h-screen bg-[#0d1117] flex justify-center p-6 pt-12">
<div className="w-full max-w-lg">
<div
className={`relative flex items-center bg-[#161b22] border rounded-xl px-4 transition-colors ${focused ? "border-[#58a6ff]" : "border-[#30363d]"}`}
>
<svg
className="w-4 h-4 text-[#8b949e] flex-shrink-0"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
placeholder="Search components, animations…"
className="flex-1 bg-transparent text-[#e6edf3] placeholder-[#484f58] px-3 py-3 text-sm focus:outline-none"
/>
{loading && (
<div className="w-4 h-4 border-2 border-[#30363d] border-t-[#58a6ff] rounded-full animate-spin flex-shrink-0" />
)}
{query && !loading && (
<button
onClick={() => setQuery("")}
className="text-[#484f58] hover:text-[#8b949e] flex-shrink-0"
>
✕
</button>
)}
</div>
<div className="mt-2 text-[11px] text-[#484f58] px-1">
{query
? `${results.length} result${results.length !== 1 ? "s" : ""} for "${query}"`
: `${ITEMS.length} components`}
</div>
<div className="mt-3 space-y-2">
{results.map((item) => (
<div
key={item.id}
className="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 flex items-center gap-3 hover:border-[#8b949e]/40 transition-colors cursor-pointer"
>
<div className="flex-1">
<p className="text-[#e6edf3] text-sm font-medium">{item.title}</p>
<p className="text-[#484f58] text-xs mt-0.5">{item.tags.join(" · ")}</p>
</div>
<span
className="text-[10px] font-semibold px-2 py-0.5 rounded-full"
style={{
color: CATEGORY_COLORS[item.category],
background: `${CATEGORY_COLORS[item.category]}18`,
}}
>
{item.category}
</span>
</div>
))}
{results.length === 0 && (
<div className="text-center py-12 text-[#484f58]">
<p className="text-3xl mb-2">🔍</p>
<p className="text-sm">No results found</p>
</div>
)}
</div>
</div>
</div>
);
}<script>
import { onDestroy } from "svelte";
const ITEMS = [
{ id: 1, title: "Command Palette", category: "UI Component", tags: ["keyboard", "search"] },
{ id: 2, title: "Animated Tabs", category: "UI Component", tags: ["tabs", "animation"] },
{ id: 3, title: "Drag & Drop List", category: "UI Component", tags: ["drag", "sortable"] },
{ id: 4, title: "CSS Typewriter", category: "Animation", tags: ["text", "css"] },
{ id: 5, title: "SVG Path Drawing", category: "Animation", tags: ["svg", "gsap"] },
{ id: 6, title: "Mouse Trail", category: "Animation", tags: ["canvas", "mouse"] },
{ id: 7, title: "Kanban Board", category: "SaaS", tags: ["drag", "project"] },
{ id: 8, title: "Calendar View", category: "SaaS", tags: ["dates", "scheduler"] },
{ id: 9, title: "Data Table", category: "UI Component", tags: ["table", "sort"] },
{ id: 10, title: "Audio Player", category: "Media", tags: ["audio", "html5"] },
{ id: 11, title: "Video Player", category: "Media", tags: ["video", "html5"] },
{ id: 12, title: "Quiz Widget", category: "Interactive", tags: ["quiz", "learning"] },
];
const CATEGORY_COLORS = {
"UI Component": "#58a6ff",
Animation: "#bc8cff",
SaaS: "#7ee787",
Media: "#f1e05a",
Interactive: "#ff7b72",
};
let query = "";
let results = ITEMS;
let loading = false;
let focused = false;
let timeoutId = null;
function search(q) {
const lower = q.toLowerCase().trim();
return ITEMS.filter(
(item) =>
!lower ||
item.title.toLowerCase().includes(lower) ||
item.category.toLowerCase().includes(lower) ||
item.tags.some((t) => t.includes(lower))
);
}
$: {
if (!query) {
results = ITEMS;
loading = false;
} else {
loading = true;
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
results = search(query);
loading = false;
}, 300);
}
}
onDestroy(() => {
if (timeoutId) clearTimeout(timeoutId);
});
</script>
<div class="min-h-screen bg-[#0d1117] flex justify-center p-6 pt-12">
<div class="w-full max-w-lg">
<div
class="relative flex items-center bg-[#161b22] border rounded-xl px-4 transition-colors"
class:border-[#58a6ff]={focused}
class:border-[#30363d]={!focused}
>
<svg class="w-4 h-4 text-[#8b949e] flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
bind:value={query}
on:focus={() => (focused = true)}
on:blur={() => (focused = false)}
placeholder="Search components, animations..."
class="flex-1 bg-transparent text-[#e6edf3] placeholder-[#484f58] px-3 py-3 text-sm focus:outline-none"
/>
{#if loading}
<div class="w-4 h-4 border-2 border-[#30363d] border-t-[#58a6ff] rounded-full animate-spin flex-shrink-0"></div>
{/if}
{#if query && !loading}
<button on:click={() => (query = "")} class="text-[#484f58] hover:text-[#8b949e] flex-shrink-0">
✕
</button>
{/if}
</div>
<div class="mt-2 text-[11px] text-[#484f58] px-1">
{#if query}
{results.length} result{results.length !== 1 ? "s" : ""} for "{query}"
{:else}
{ITEMS.length} components
{/if}
</div>
<div class="mt-3 space-y-2">
{#each results as item (item.id)}
<div
class="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 flex items-center gap-3 hover:border-[#8b949e]/40 transition-colors cursor-pointer"
>
<div class="flex-1">
<p class="text-[#e6edf3] text-sm font-medium">{item.title}</p>
<p class="text-[#484f58] text-xs mt-0.5">{item.tags.join(" \u00B7 ")}</p>
</div>
<span
class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
style="color: {CATEGORY_COLORS[item.category]}; background: {CATEGORY_COLORS[item.category]}18;"
>
{item.category}
</span>
</div>
{/each}
{#if results.length === 0}
<div class="text-center py-12 text-[#484f58]">
<p class="text-3xl mb-2">🔍</p>
<p class="text-sm">No results found</p>
</div>
{/if}
</div>
</div>
</div><script setup>
import { ref, watch, onUnmounted } from "vue";
const ITEMS = [
{ id: 1, title: "Command Palette", category: "UI Component", tags: ["keyboard", "search"] },
{ id: 2, title: "Animated Tabs", category: "UI Component", tags: ["tabs", "animation"] },
{ id: 3, title: "Drag & Drop List", category: "UI Component", tags: ["drag", "sortable"] },
{ id: 4, title: "CSS Typewriter", category: "Animation", tags: ["text", "css"] },
{ id: 5, title: "SVG Path Drawing", category: "Animation", tags: ["svg", "gsap"] },
{ id: 6, title: "Mouse Trail", category: "Animation", tags: ["canvas", "mouse"] },
{ id: 7, title: "Kanban Board", category: "SaaS", tags: ["drag", "project"] },
{ id: 8, title: "Calendar View", category: "SaaS", tags: ["dates", "scheduler"] },
{ id: 9, title: "Data Table", category: "UI Component", tags: ["table", "sort"] },
{ id: 10, title: "Audio Player", category: "Media", tags: ["audio", "html5"] },
{ id: 11, title: "Video Player", category: "Media", tags: ["video", "html5"] },
{ id: 12, title: "Quiz Widget", category: "Interactive", tags: ["quiz", "learning"] },
];
const CATEGORY_COLORS = {
"UI Component": "#58a6ff",
Animation: "#bc8cff",
SaaS: "#7ee787",
Media: "#f1e05a",
Interactive: "#ff7b72",
};
const query = ref("");
const results = ref(ITEMS);
const loading = ref(false);
const focused = ref(false);
let timeoutId = null;
function search(q) {
const lower = q.toLowerCase().trim();
return ITEMS.filter(
(item) =>
!lower ||
item.title.toLowerCase().includes(lower) ||
item.category.toLowerCase().includes(lower) ||
item.tags.some((t) => t.includes(lower))
);
}
watch(query, (q) => {
if (!q) {
results.value = ITEMS;
loading.value = false;
return;
}
loading.value = true;
if (timeoutId) clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
results.value = search(q);
loading.value = false;
}, 300);
});
onUnmounted(() => {
if (timeoutId) clearTimeout(timeoutId);
});
</script>
<template>
<div class="min-h-screen bg-[#0d1117] flex justify-center p-6 pt-12">
<div class="w-full max-w-lg">
<div
class="relative flex items-center bg-[#161b22] border rounded-xl px-4 transition-colors"
:class="focused ? 'border-[#58a6ff]' : 'border-[#30363d]'"
>
<svg class="w-4 h-4 text-[#8b949e] flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
v-model="query"
@focus="focused = true"
@blur="focused = false"
placeholder="Search components, animations..."
class="flex-1 bg-transparent text-[#e6edf3] placeholder-[#484f58] px-3 py-3 text-sm focus:outline-none"
/>
<div
v-if="loading"
class="w-4 h-4 border-2 border-[#30363d] border-t-[#58a6ff] rounded-full animate-spin flex-shrink-0"
></div>
<button
v-if="query && !loading"
@click="query = ''"
class="text-[#484f58] hover:text-[#8b949e] flex-shrink-0"
>
✕
</button>
</div>
<div class="mt-2 text-[11px] text-[#484f58] px-1">
<template v-if="query">
{{ results.length }} result{{ results.length !== 1 ? 's' : '' }} for "{{ query }}"
</template>
<template v-else>{{ ITEMS.length }} components</template>
</div>
<div class="mt-3 space-y-2">
<div
v-for="item in results"
:key="item.id"
class="bg-[#161b22] border border-[#30363d] rounded-xl px-4 py-3 flex items-center gap-3 hover:border-[#8b949e]/40 transition-colors cursor-pointer"
>
<div class="flex-1">
<p class="text-[#e6edf3] text-sm font-medium">{{ item.title }}</p>
<p class="text-[#484f58] text-xs mt-0.5">{{ item.tags.join(' \u00B7 ') }}</p>
</div>
<span
class="text-[10px] font-semibold px-2 py-0.5 rounded-full"
:style="{ color: CATEGORY_COLORS[item.category], background: CATEGORY_COLORS[item.category] + '18' }"
>
{{ item.category }}
</span>
</div>
<div v-if="results.length === 0" class="text-center py-12 text-[#484f58]">
<p class="text-3xl mb-2">🔍</p>
<p class="text-sm">No results found</p>
</div>
</div>
</div>
</div>
</template>Live Search with Debounce
A high-performance search component that provides instant feedback. It uses a custom debounce function to ensure that search logic only executes after the user has finished typing, preventing laggy UIs and excessive API calls.
Features
- Instant results filtering
- Performance-optimized debouncing
- Visual feedback during “searching” state
- Clear search functionality
- Keyboard navigation friendly
- Empty state handling