Streaming — Search / Category Grid
A cinematic dark-mode search and browse screen for the fictional Lumora streaming service. A pill search field surfaces live, keyboard-navigable suggestions for titles and genres with highlighted matches, while genre filter chips and a sort menu reshape a responsive poster grid in real time. Hovering a poster scales it and reveals a play button plus a match score, and graceful loading skeletons and an empty state keep the experience smooth on every query.
MCP
Code
:root {
--bg: #0b0b0f;
--surface: #15151c;
--surface-2: #1e1e27;
--ink: #f4f4f7;
--ink-2: #b6b7c3;
--muted: #83859a;
--brand: #7c5cff;
--brand-2: #e50914;
--accent: #ffffff;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.16);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 18px;
--shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
--glow: 0 0 0 1px rgba(124, 92, 255, 0.5), 0 14px 38px rgba(124, 92, 255, 0.28);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
background:
radial-gradient(1100px 520px at 88% -8%, rgba(124, 92, 255, 0.16), transparent 60%),
radial-gradient(900px 480px at 6% 0%, rgba(229, 9, 20, 0.1), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
min-height: 100vh;
}
img { max-width: 100%; display: block; }
.skip {
position: absolute;
left: -999px;
top: 0;
background: var(--brand);
color: #fff;
padding: 10px 16px;
border-radius: var(--r-sm);
z-index: 50;
}
.skip:focus { left: 12px; top: 12px; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- Top nav ---------- */
.topnav {
position: sticky;
top: 0;
z-index: 30;
backdrop-filter: blur(14px);
background: linear-gradient(180deg, rgba(11, 11, 15, 0.92), rgba(11, 11, 15, 0.55) 70%, transparent);
transition: background 0.3s ease;
}
.topnav.scrolled { background: rgba(11, 11, 15, 0.95); border-bottom: 1px solid var(--line); }
.nav-inner {
max-width: 1280px;
margin: 0 auto;
padding: 16px 24px;
display: flex;
align-items: center;
gap: 28px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 800;
letter-spacing: 0.22em;
font-size: 18px;
color: var(--ink);
text-decoration: none;
}
.brand-mark {
width: 18px;
height: 18px;
border-radius: 5px;
background: linear-gradient(135deg, var(--brand), var(--brand-2));
box-shadow: 0 0 16px rgba(124, 92, 255, 0.7);
}
.nav-links {
display: flex;
gap: 22px;
margin-right: auto;
}
.nav-links a {
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: color 0.15s ease;
}
.nav-links a:hover,
.nav-links a[aria-current="page"] { color: var(--ink); }
.nav-right { display: flex; align-items: center; gap: 14px; }
.badge-plan {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
color: var(--ink-2);
border: 1px solid var(--line-2);
border-radius: 999px;
padding: 5px 10px;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 9px;
border: none;
font-weight: 700;
font-size: 13px;
color: #fff;
background: linear-gradient(135deg, #5a3cff, #b6207f);
cursor: pointer;
}
/* ---------- Search ---------- */
main {
max-width: 1280px;
margin: 0 auto;
padding: 8px 24px 80px;
}
.searchbar { padding: 22px 0 8px; }
.search-wrap { position: relative; max-width: 720px; }
.search-field {
display: flex;
align-items: center;
gap: 12px;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-lg);
padding: 0 16px;
height: 56px;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.search-field:focus-within { border-color: var(--brand); box-shadow: var(--glow); }
.search-ico { width: 22px; height: 22px; color: var(--muted); flex: none; }
.search-input {
flex: 1;
border: none;
background: none;
color: var(--ink);
font: inherit;
font-size: 16px;
outline: none;
min-width: 0;
}
.search-input::placeholder { color: var(--muted); }
.search-input::-webkit-search-cancel-button { display: none; }
.search-clear {
border: none;
background: var(--surface-2);
color: var(--ink-2);
width: 28px;
height: 28px;
border-radius: 50%;
font-size: 18px;
line-height: 1;
cursor: pointer;
flex: none;
}
.search-clear:hover { color: var(--ink); }
.suggest {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
list-style: none;
margin: 0;
padding: 6px;
background: var(--surface);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
box-shadow: var(--shadow);
z-index: 25;
max-height: 320px;
overflow-y: auto;
}
.suggest li {
padding: 10px 12px;
border-radius: var(--r-sm);
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
font-size: 14px;
}
.suggest li[aria-selected="true"],
.suggest li:hover { background: var(--surface-2); }
.suggest .s-ico { color: var(--muted); display: inline-flex; }
.suggest .s-meta { margin-left: auto; color: var(--muted); font-size: 12px; }
.suggest mark { background: rgba(124, 92, 255, 0.3); color: var(--ink); border-radius: 3px; padding: 0 1px; }
/* ---------- Controls ---------- */
.controls {
display: flex;
align-items: center;
gap: 18px;
flex-wrap: wrap;
padding: 18px 0 4px;
}
.chips {
display: flex;
gap: 9px;
flex-wrap: wrap;
margin-right: auto;
}
.chip {
border: 1px solid var(--line-2);
background: var(--surface);
color: var(--ink-2);
padding: 8px 15px;
border-radius: 999px;
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
}
.chip:hover { border-color: var(--line-2); color: var(--ink); transform: translateY(-1px); }
.chip[aria-selected="true"] {
background: linear-gradient(135deg, var(--brand), #9a7bff);
border-color: transparent;
color: #fff;
box-shadow: 0 8px 20px rgba(124, 92, 255, 0.35);
}
.sort { display: flex; align-items: center; gap: 10px; }
.sort-label { color: var(--muted); font-size: 13px; font-weight: 500; }
.select-wrap { position: relative; display: inline-flex; align-items: center; }
.select-wrap svg { position: absolute; right: 10px; width: 18px; height: 18px; color: var(--muted); pointer-events: none; }
.select-wrap select {
appearance: none;
-webkit-appearance: none;
background: var(--surface);
color: var(--ink);
border: 1px solid var(--line-2);
border-radius: var(--r-sm);
padding: 9px 34px 9px 14px;
font: inherit;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
/* ---------- Results head ---------- */
.results-head {
display: flex;
align-items: baseline;
gap: 14px;
padding: 26px 0 14px;
}
.results-head h1 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -0.01em; }
.count { margin: 0; color: var(--muted); font-size: 14px; }
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 18px;
}
.card {
position: relative;
border-radius: var(--r-md);
overflow: hidden;
background: var(--surface);
border: 1px solid var(--line);
cursor: pointer;
transition: transform 0.22s cubic-bezier(.2,.7,.2,1), box-shadow 0.22s ease, z-index 0s;
animation: pop 0.32s ease both;
}
@keyframes pop { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
.card:hover,
.card:focus-within {
transform: translateY(-6px) scale(1.035);
box-shadow: var(--shadow);
z-index: 5;
border-color: var(--line-2);
}
.poster {
position: relative;
aspect-ratio: 2 / 3;
display: grid;
place-items: center;
padding: 14px;
overflow: hidden;
}
.poster::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(180deg, transparent 45%, rgba(0,0,0,0.78));
}
.poster-title {
position: relative;
z-index: 1;
font-weight: 800;
font-size: 20px;
letter-spacing: 0.02em;
text-align: center;
text-shadow: 0 2px 14px rgba(0,0,0,0.6);
color: #fff;
line-height: 1.15;
}
.badges {
position: absolute;
top: 10px;
left: 10px;
z-index: 2;
display: flex;
gap: 6px;
}
.qbadge {
font-size: 10px;
font-weight: 800;
letter-spacing: 0.04em;
padding: 3px 6px;
border-radius: 5px;
background: rgba(0,0,0,0.55);
border: 1px solid var(--line-2);
backdrop-filter: blur(4px);
}
.qbadge.new { background: var(--brand-2); border-color: transparent; }
.hover-meta {
position: absolute;
inset: auto 0 0 0;
z-index: 2;
padding: 12px;
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
transform: translateY(6px);
transition: opacity 0.2s ease, transform 0.2s ease;
}
.card:hover .hover-meta,
.card:focus-within .hover-meta { opacity: 1; transform: none; }
.play-btn {
width: 34px;
height: 34px;
border-radius: 50%;
background: #fff;
border: none;
display: grid;
place-items: center;
cursor: pointer;
flex: none;
}
.play-btn svg { width: 16px; height: 16px; color: #0b0b0f; margin-left: 2px; }
.info {
padding: 11px 13px 14px;
display: flex;
flex-direction: column;
gap: 4px;
}
.info .t { font-weight: 600; font-size: 14px; }
.info .sub { color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dot { width: 3px; height: 3px; border-radius: 50%; background: currentColor; }
.rating { color: #ffd166; font-weight: 700; }
.match { color: #6dd58c; font-weight: 700; }
/* ---------- Skeleton / loading ---------- */
.skeleton .poster,
.skeleton .info .t,
.skeleton .info .sub {
background: linear-gradient(100deg, var(--surface-2) 30%, #2a2a36 50%, var(--surface-2) 70%);
background-size: 200% 100%;
animation: shimmer 1.2s linear infinite;
color: transparent !important;
border-radius: 6px;
}
.skeleton { pointer-events: none; cursor: default; animation: none; }
.skeleton .poster::after, .skeleton .badges, .skeleton .poster-title { display: none; }
.skeleton .info .t { height: 14px; width: 70%; }
.skeleton .info .sub { height: 11px; width: 45%; }
@keyframes shimmer { from { background-position: 200% 0; } to { background-position: -200% 0; } }
/* ---------- Empty ---------- */
.empty {
text-align: center;
padding: 70px 20px;
color: var(--ink-2);
}
.empty-art { font-size: 52px; margin-bottom: 12px; }
.empty h2 { margin: 0 0 6px; color: var(--ink); font-size: 20px; }
.empty p { margin: 0 auto 20px; max-width: 360px; }
.btn {
border: none;
background: linear-gradient(135deg, var(--brand), #9a7bff);
color: #fff;
font: inherit;
font-weight: 700;
padding: 11px 22px;
border-radius: var(--r-sm);
cursor: pointer;
}
.btn:hover { filter: brightness(1.08); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--surface-2);
color: var(--ink);
border: 1px solid var(--line-2);
padding: 12px 18px;
border-radius: var(--r-md);
box-shadow: var(--shadow);
font-size: 14px;
font-weight: 500;
opacity: 0;
pointer-events: none;
transition: opacity 0.25s ease, transform 0.25s ease;
z-index: 60;
max-width: calc(100vw - 40px);
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }
/* ---------- Responsive ---------- */
@media (max-width: 880px) {
.nav-links { display: none; }
}
@media (max-width: 520px) {
.nav-inner { padding: 13px 16px; gap: 14px; }
main { padding: 6px 16px 64px; }
.badge-plan { display: none; }
.search-field { height: 50px; }
.search-input { font-size: 15px; }
.grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; }
.poster-title { font-size: 16px; }
.controls { gap: 12px; }
.results-head { padding: 20px 0 12px; }
.results-head h1 { font-size: 19px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}(function () {
"use strict";
/* ---------- Data (fictional catalog) ---------- */
var CATALOG = [
{ t: "Neon Harbor", genre: "Sci-Fi", year: 2025, rating: 8.7, match: 97, q: "4K", g1: "#1b2a4a", g2: "#5a3cff", tag: "new" },
{ t: "The Quiet Mile", genre: "Drama", year: 2024, rating: 8.2, match: 91, q: "HD", g1: "#3a2a1a", g2: "#b5651d" },
{ t: "Crimson Static", genre: "Thriller", year: 2025, rating: 7.9, match: 88, q: "4K", g1: "#3a0d12", g2: "#e50914", tag: "new" },
{ t: "Paper Lanterns", genre: "Romance", year: 2023, rating: 7.6, match: 84, q: "HD", g1: "#3a1230", g2: "#ff6ad5" },
{ t: "Glass Continent", genre: "Documentary", year: 2024, rating: 8.5, match: 79, q: "4K", g1: "#0d2a2a", g2: "#16a085" },
{ t: "Howl & Hollow", genre: "Horror", year: 2025, rating: 7.1, match: 73, q: "HD", g1: "#1a0d1a", g2: "#7c2cff", tag: "new" },
{ t: "Midnight Cartographer", genre: "Sci-Fi", year: 2022, rating: 8.0, match: 90, q: "HD", g1: "#10233f", g2: "#2f80ed" },
{ t: "Salt & Vinegar Club", genre: "Comedy", year: 2024, rating: 7.4, match: 81, q: "HD", g1: "#2a2a0d", g2: "#f1c40f" },
{ t: "Ironwood", genre: "Drama", year: 2021, rating: 8.9, match: 95, q: "4K", g1: "#1d2a1a", g2: "#27ae60" },
{ t: "Velvet Frequency", genre: "Thriller", year: 2023, rating: 7.8, match: 86, q: "4K", g1: "#2a0d2a", g2: "#9b59b6" },
{ t: "The Last Aurora", genre: "Sci-Fi", year: 2025, rating: 8.4, match: 93, q: "4K", g1: "#0d2030", g2: "#00d2d3", tag: "new" },
{ t: "Sundown Diner", genre: "Romance", year: 2022, rating: 7.3, match: 77, q: "HD", g1: "#3a1d0d", g2: "#e67e22" },
{ t: "Hollow Tide", genre: "Horror", year: 2024, rating: 7.0, match: 70, q: "HD", g1: "#0d1a2a", g2: "#34495e" },
{ t: "Bureau of Lost Stars", genre: "Documentary", year: 2025, rating: 8.6, match: 82, q: "4K", g1: "#1a1a3a", g2: "#5a6bff", tag: "new" },
{ t: "Carousel of Crows", genre: "Thriller", year: 2021, rating: 7.7, match: 85, q: "HD", g1: "#1a0d0d", g2: "#c0392b" },
{ t: "Two Left Feet", genre: "Comedy", year: 2023, rating: 7.2, match: 80, q: "HD", g1: "#0d2a1d", g2: "#2ecc71" },
{ t: "Cathedral of Wires", genre: "Sci-Fi", year: 2024, rating: 8.1, match: 89, q: "4K", g1: "#1a103a", g2: "#8e44ff" },
{ t: "The Ash Garden", genre: "Drama", year: 2025, rating: 8.3, match: 92, q: "4K", g1: "#2a1a0d", g2: "#d35400", tag: "new" },
{ t: "Whisper Network", genre: "Thriller", year: 2022, rating: 7.5, match: 83, q: "HD", g1: "#0d1a1a", g2: "#1abc9c" },
{ t: "Marigold Heist", genre: "Comedy", year: 2024, rating: 7.6, match: 78, q: "HD", g1: "#2a230d", g2: "#f39c12" },
{ t: "Beneath the Pier", genre: "Horror", year: 2023, rating: 6.9, match: 68, q: "HD", g1: "#0d1530", g2: "#34568b" },
{ t: "Orbit & Ember", genre: "Romance", year: 2025, rating: 7.9, match: 87, q: "4K", g1: "#3a0d24", g2: "#ff5e8a", tag: "new" },
{ t: "The Tideglass Files", genre: "Documentary", year: 2022, rating: 8.0, match: 75, q: "HD", g1: "#0d2a26", g2: "#16c79a" },
{ t: "Nightshade Avenue", genre: "Thriller", year: 2024, rating: 8.2, match: 90, q: "4K", g1: "#1a0d2a", g2: "#6c5ce7" }
];
var GENRES = ["All", "Sci-Fi", "Thriller", "Drama", "Romance", "Comedy", "Horror", "Documentary"];
/* ---------- State ---------- */
var state = { query: "", genre: "All", sort: "trending", active: -1 };
/* ---------- Elements ---------- */
var $ = function (s) { return document.querySelector(s); };
var els = {
search: $("#search"),
clear: $("#clear"),
suggest: $("#suggest"),
chips: $("#chips"),
sort: $("#sort"),
grid: $("#results"),
title: $("#results-title"),
count: $("#count"),
empty: $("#empty"),
reset: $("#reset"),
topnav: $("#topnav"),
toast: $("#toast")
};
/* ---------- Toast ---------- */
var toastTimer;
function toast(msg) {
els.toast.textContent = msg;
els.toast.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () { els.toast.classList.remove("show"); }, 2200);
}
/* ---------- Helpers ---------- */
function esc(s) { return s.replace(/[&<>"]/g, function (c) { return ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]; }); }
function highlight(text, q) {
if (!q) return esc(text);
var i = text.toLowerCase().indexOf(q.toLowerCase());
if (i < 0) return esc(text);
return esc(text.slice(0, i)) + "<mark>" + esc(text.slice(i, i + q.length)) + "</mark>" + esc(text.slice(i + q.length));
}
function filtered() {
var q = state.query.trim().toLowerCase();
var list = CATALOG.filter(function (m) {
var byGenre = state.genre === "All" || m.genre === state.genre;
var byQuery = !q || m.t.toLowerCase().indexOf(q) >= 0 || m.genre.toLowerCase().indexOf(q) >= 0;
return byGenre && byQuery;
});
var s = state.sort;
list.sort(function (a, b) {
if (s === "rating") return b.rating - a.rating;
if (s === "year") return b.year - a.year;
if (s === "az") return a.t.localeCompare(b.t);
return b.match - a.match; // trending
});
return list;
}
/* ---------- Chips ---------- */
function buildChips() {
GENRES.forEach(function (g) {
var b = document.createElement("button");
b.className = "chip";
b.type = "button";
b.setAttribute("role", "tab");
b.textContent = g;
b.setAttribute("aria-selected", g === state.genre ? "true" : "false");
b.addEventListener("click", function () {
state.genre = g;
Array.prototype.forEach.call(els.chips.children, function (c) {
c.setAttribute("aria-selected", c.textContent === g ? "true" : "false");
});
renderWithLoading();
});
els.chips.appendChild(b);
});
}
/* ---------- Cards ---------- */
function cardHTML(m) {
var badges = '<span class="qbadge">' + m.q + "</span>";
if (m.tag === "new") badges += '<span class="qbadge new">NEW</span>';
return (
'<article class="card" tabindex="0" role="button" aria-label="' + esc(m.t) + ', ' + m.genre + ', rated ' + m.rating + '">' +
'<div class="poster" style="background:linear-gradient(150deg,' + m.g1 + ',' + m.g2 + ')">' +
'<div class="badges">' + badges + "</div>" +
'<div class="poster-title">' + esc(m.t) + "</div>" +
'<div class="hover-meta">' +
'<button class="play-btn" type="button" aria-label="Play ' + esc(m.t) + '"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg></button>' +
'<span class="match">' + m.match + "% match</span>" +
"</div>" +
"</div>" +
'<div class="info">' +
'<span class="t">' + esc(m.t) + "</span>" +
'<span class="sub"><span class="rating">★ ' + m.rating.toFixed(1) + "</span><span class=\"dot\"></span>" + m.year + '<span class="dot"></span>' + m.genre + "</span>" +
"</div>" +
"</article>"
);
}
function skeletonHTML() {
return '<article class="card skeleton"><div class="poster"></div><div class="info"><span class="t"></span><span class="sub"></span></div></article>';
}
/* ---------- Render ---------- */
function render() {
var list = filtered();
if (!list.length) {
els.grid.innerHTML = "";
els.empty.hidden = false;
} else {
els.empty.hidden = true;
els.grid.innerHTML = list.map(cardHTML).join("");
}
els.grid.setAttribute("aria-busy", "false");
var q = state.query.trim();
els.title.textContent = q ? 'Results for "' + q + '"' : (state.genre === "All" ? "Browse everything" : state.genre);
els.count.textContent = list.length + (list.length === 1 ? " title" : " titles");
Array.prototype.forEach.call(els.grid.querySelectorAll(".card"), function (card, i) {
var title = list[i].t;
card.addEventListener("click", function (e) {
if (e.target.closest(".play-btn")) { toast("▶ Playing “" + title + "”"); return; }
toast("Opening “" + title + "”");
});
card.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") { e.preventDefault(); toast("Opening “" + title + "”"); }
});
});
}
var loadTimer;
function renderWithLoading() {
closeSuggest();
els.grid.setAttribute("aria-busy", "true");
els.empty.hidden = true;
var n = Math.max(filtered().length, 6);
var skel = "";
for (var i = 0; i < Math.min(n, 12); i++) skel += skeletonHTML();
els.grid.innerHTML = skel;
clearTimeout(loadTimer);
loadTimer = setTimeout(render, 320);
}
/* ---------- Suggestions ---------- */
function suggestions() {
var q = state.query.trim().toLowerCase();
if (!q) return [];
var out = [];
GENRES.forEach(function (g) {
if (g !== "All" && g.toLowerCase().indexOf(q) >= 0) out.push({ label: g, type: "genre" });
});
CATALOG.forEach(function (m) {
if (m.t.toLowerCase().indexOf(q) >= 0) out.push({ label: m.t, type: "title", meta: m.year + " · " + m.genre });
});
return out.slice(0, 7);
}
function ICON(type) {
if (type === "genre") return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M4 12h16M4 17h10"/></svg>';
return '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M8 4v16M16 4v16M3 9h5M16 9h5M3 15h5M16 15h5"/></svg>';
}
function renderSuggest() {
var items = suggestions();
state.active = -1;
if (!items.length) { closeSuggest(); return; }
var q = state.query.trim();
els.suggest.innerHTML = items.map(function (it, i) {
return '<li role="option" id="opt-' + i + '" aria-selected="false">' +
'<span class="s-ico">' + ICON(it.type) + "</span>" +
"<span>" + highlight(it.label, q) + "</span>" +
(it.meta ? '<span class="s-meta">' + esc(it.meta) + "</span>" : '<span class="s-meta">Genre</span>') +
"</li>";
}).join("");
els.suggest.hidden = false;
els.search.setAttribute("aria-expanded", "true");
Array.prototype.forEach.call(els.suggest.children, function (li, i) {
li.addEventListener("mousedown", function (e) {
e.preventDefault();
chooseSuggestion(items[i]);
});
});
}
function chooseSuggestion(it) {
if (it.type === "genre") {
els.search.value = "";
state.query = "";
state.genre = it.label;
Array.prototype.forEach.call(els.chips.children, function (c) {
c.setAttribute("aria-selected", c.textContent === it.label ? "true" : "false");
});
} else {
els.search.value = it.label;
state.query = it.label;
}
els.clear.hidden = !els.search.value;
renderWithLoading();
}
function closeSuggest() {
els.suggest.hidden = true;
els.suggest.innerHTML = "";
els.search.setAttribute("aria-expanded", "false");
state.active = -1;
}
function moveActive(dir) {
var opts = els.suggest.querySelectorAll("li");
if (!opts.length) return;
if (opts[state.active]) opts[state.active].setAttribute("aria-selected", "false");
state.active = (state.active + dir + opts.length) % opts.length;
opts[state.active].setAttribute("aria-selected", "true");
els.search.setAttribute("aria-activedescendant", "opt-" + state.active);
opts[state.active].scrollIntoView({ block: "nearest" });
}
/* ---------- Events ---------- */
var debTimer;
els.search.addEventListener("input", function () {
state.query = els.search.value;
els.clear.hidden = !els.search.value;
renderSuggest();
clearTimeout(debTimer);
debTimer = setTimeout(renderWithLoading, 220);
});
els.search.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown") { e.preventDefault(); if (els.suggest.hidden) renderSuggest(); else moveActive(1); }
else if (e.key === "ArrowUp") { e.preventDefault(); moveActive(-1); }
else if (e.key === "Enter") {
var opts = els.suggest.querySelectorAll("li");
if (state.active >= 0 && opts[state.active]) {
e.preventDefault();
opts[state.active].dispatchEvent(new MouseEvent("mousedown"));
} else {
clearTimeout(debTimer);
renderWithLoading();
}
} else if (e.key === "Escape") {
if (!els.suggest.hidden) { closeSuggest(); }
else if (els.search.value) { els.search.value = ""; state.query = ""; els.clear.hidden = true; renderWithLoading(); }
}
});
els.search.addEventListener("blur", function () { setTimeout(closeSuggest, 120); });
els.clear.addEventListener("click", function () {
els.search.value = "";
state.query = "";
els.clear.hidden = true;
closeSuggest();
els.search.focus();
renderWithLoading();
});
els.sort.addEventListener("change", function () {
state.sort = els.sort.value;
renderWithLoading();
toast("Sorted by " + els.sort.options[els.sort.selectedIndex].text.toLowerCase());
});
els.reset.addEventListener("click", function () {
state.query = "";
state.genre = "All";
state.sort = "trending";
els.search.value = "";
els.clear.hidden = true;
els.sort.value = "trending";
Array.prototype.forEach.call(els.chips.children, function (c) {
c.setAttribute("aria-selected", c.textContent === "All" ? "true" : "false");
});
renderWithLoading();
});
document.addEventListener("click", function (e) {
if (!e.target.closest(".search-wrap")) closeSuggest();
});
window.addEventListener("scroll", function () {
els.topnav.classList.toggle("scrolled", window.scrollY > 12);
}, { passive: true });
/* ---------- Init ---------- */
buildChips();
render();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Lumora — Search & Browse</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip" href="#results">Skip to results</a>
<header class="topnav" id="topnav">
<div class="nav-inner">
<a class="brand" href="#" aria-label="Lumora home">
<span class="brand-mark" aria-hidden="true"></span>
LUMORA
</a>
<nav class="nav-links" aria-label="Primary">
<a href="#" aria-current="page">Browse</a>
<a href="#">Series</a>
<a href="#">Films</a>
<a href="#">My List</a>
</nav>
<div class="nav-right">
<span class="badge-plan">4K · HDR</span>
<button class="avatar" type="button" aria-label="Account menu">LK</button>
</div>
</div>
</header>
<main>
<section class="searchbar" aria-label="Search the catalog">
<div class="search-wrap">
<div class="search-field">
<svg class="search-ico" viewBox="0 0 24 24" aria-hidden="true"><path d="M21 21l-4.3-4.3M11 18a7 7 0 1 1 0-14 7 7 0 0 1 0 14Z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input
id="search"
type="search"
class="search-input"
placeholder="Search titles, genres, people…"
autocomplete="off"
role="combobox"
aria-expanded="false"
aria-controls="suggest"
aria-autocomplete="list"
aria-label="Search titles, genres, people"
/>
<button id="clear" class="search-clear" type="button" aria-label="Clear search" hidden>×</button>
</div>
<ul id="suggest" class="suggest" role="listbox" aria-label="Suggestions" hidden></ul>
</div>
</section>
<section class="controls" aria-label="Filters and sorting">
<div class="chips" id="chips" role="tablist" aria-label="Genre filter"></div>
<div class="sort">
<label for="sort" class="sort-label">Sort</label>
<div class="select-wrap">
<select id="sort" aria-label="Sort results">
<option value="trending">Trending</option>
<option value="rating">Top rated</option>
<option value="year">Newest</option>
<option value="az">A–Z</option>
</select>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 9l6 6 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
</div>
</div>
</section>
<section class="results-head">
<h1 id="results-title">Browse everything</h1>
<p class="count" id="count" aria-live="polite"></p>
</section>
<section id="results" class="grid" aria-label="Search results" aria-busy="false"></section>
<div id="empty" class="empty" hidden>
<div class="empty-art" aria-hidden="true">🛰️</div>
<h2>No titles match your search</h2>
<p>Try a different keyword or clear your filters to explore the full catalog.</p>
<button id="reset" class="btn" type="button">Reset filters</button>
</div>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Search / Category Grid
A browse-and-search screen for the fictional Lumora streaming service, built dark-first with a fading top nav, a violet-and-red signature palette, and CSS-gradient posters — no external images. The hero is the search experience: a large pill input that opens a live suggestion dropdown as you type, mixing genre and title matches, highlighting the typed substring, and showing year/genre metadata for each result.
Everything filters instantly. Type to narrow the grid, tap genre chips to switch categories, or change the sort menu between trending, top-rated, newest, and A–Z. Each query briefly shows shimmering skeleton cards before the real posters animate in, and a friendly empty state appears with a reset button when nothing matches. Poster cards scale on hover to reveal a play button and a match score, and carry HD/4K and NEW quality badges.
The suggestion list is fully keyboard-operable — arrow keys move the active option, Enter selects it, and Escape clears — with proper combobox ARIA, focusable cards, visible focus rings, a toast helper for actions, and a layout that collapses cleanly from desktop down to ~360px.
Illustrative UI only — fictional titles, not a real streaming service.