Music — Search (artists · albums · tracks)
A tactile, Spotify-style music search built in vanilla JS. A prominent rounded input with an animated search icon drives debounced live filtering over a fictional catalog of artists, albums, songs and playlists. Results group into a themed Top result card plus Songs, Artists, Albums and Playlists sections, with a category chip row to narrow scope. Matched substrings highlight in the accent color, recent searches persist with clear-all, and arrow-key navigation plus Enter triggers simulated playback with animated equalizers, play counts and durations.
MCP
الكود
:root {
--bg: #0b0b0f;
--bg-2: #13131a;
--surface: #1a1a22;
--surface-2: #22222c;
--text: #f4f4f7;
--muted: #a0a0ad;
--line: rgba(255, 255, 255, 0.1);
--line-2: rgba(255, 255, 255, 0.18);
--accent: #1db954;
--accent-2: #8b5cf6;
--accent-3: #ff3d71;
--r-sm: 8px;
--r-md: 14px;
--r-lg: 20px;
--r-full: 999px;
--shadow: 0 16px 40px rgba(0, 0, 0, 0.5);
--shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.35);
--font-display: "Sora", system-ui, sans-serif;
--font-body: "Inter", system-ui, sans-serif;
}
* {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(1100px 520px at 12% -10%, rgba(139, 92, 246, 0.16), transparent 60%),
radial-gradient(900px 480px at 100% 0%, rgba(29, 185, 84, 0.12), transparent 58%),
var(--bg);
color: var(--text);
font-family: var(--font-body);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.app {
max-width: 760px;
margin: 0 auto;
padding: 32px 20px 80px;
}
/* ---------- header ---------- */
.app__head {
margin-bottom: 22px;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
}
.brand__mark {
display: grid;
place-items: center;
width: 42px;
height: 42px;
border-radius: var(--r-md);
background: linear-gradient(140deg, var(--accent), var(--accent-2));
box-shadow: var(--shadow-sm);
}
.eq {
display: flex;
align-items: flex-end;
gap: 2.5px;
height: 18px;
}
.eq i {
width: 3px;
height: 40%;
border-radius: 2px;
background: #08130c;
animation: eq 0.9s ease-in-out infinite;
}
.eq i:nth-child(2) { animation-delay: 0.2s; }
.eq i:nth-child(3) { animation-delay: 0.4s; }
.eq i:nth-child(4) { animation-delay: 0.1s; }
@keyframes eq {
0%, 100% { height: 30%; }
50% { height: 100%; }
}
.brand__name {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.35rem;
letter-spacing: -0.02em;
}
.app__sub {
margin: 10px 2px 0;
color: var(--muted);
font-size: 0.92rem;
}
/* ---------- search ---------- */
.search {
position: sticky;
top: 12px;
z-index: 5;
}
.search__field {
display: flex;
align-items: center;
gap: 10px;
padding: 0 12px 0 16px;
height: 56px;
border-radius: var(--r-full);
background: var(--surface);
border: 1px solid var(--line);
box-shadow: var(--shadow-sm);
transition: border-color 0.18s, box-shadow 0.18s, background 0.18s;
}
.search__field:focus-within {
border-color: var(--accent);
background: var(--surface-2);
box-shadow: 0 0 0 4px rgba(29, 185, 84, 0.16), var(--shadow-sm);
}
.search__icon {
display: grid;
place-items: center;
color: var(--muted);
transition: color 0.18s, transform 0.18s;
flex-shrink: 0;
}
.search__field:focus-within .search__icon {
color: var(--accent);
transform: scale(1.06) rotate(-6deg);
}
.search__input {
flex: 1;
min-width: 0;
border: 0;
outline: none;
background: transparent;
color: var(--text);
font-family: var(--font-body);
font-size: 1.02rem;
font-weight: 500;
}
.search__input::placeholder {
color: var(--muted);
}
.search__clear {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border: 0;
border-radius: var(--r-full);
background: var(--surface-2);
color: var(--muted);
cursor: pointer;
transition: color 0.16s, background 0.16s, transform 0.16s;
}
.search__clear:hover {
color: var(--text);
background: var(--line-2);
}
.search__clear:active { transform: scale(0.9); }
/* ---------- chips ---------- */
.chips {
display: flex;
gap: 8px;
margin-top: 14px;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
flex-shrink: 0;
padding: 8px 16px;
border-radius: var(--r-full);
border: 1px solid var(--line);
background: var(--surface);
color: var(--muted);
font-family: var(--font-body);
font-size: 0.86rem;
font-weight: 600;
cursor: pointer;
transition: color 0.16s, background 0.16s, border-color 0.16s, transform 0.12s;
}
.chip:hover {
color: var(--text);
border-color: var(--line-2);
}
.chip:active { transform: scale(0.96); }
.chip.is-active {
background: var(--text);
color: #0b0b0f;
border-color: var(--text);
}
.chip:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
/* ---------- results ---------- */
.results {
margin-top: 26px;
display: flex;
flex-direction: column;
gap: 28px;
}
.group__title {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.18rem;
letter-spacing: -0.01em;
margin: 0 0 12px;
}
.group__list {
display: flex;
flex-direction: column;
gap: 4px;
}
mark {
background: transparent;
color: var(--accent);
font-weight: 700;
}
/* ---------- top result ---------- */
.top {
display: grid;
grid-template-columns: 132px 1fr;
gap: 18px;
padding: 20px;
border-radius: var(--r-lg);
background: linear-gradient(160deg, var(--surface-2), var(--surface));
border: 1px solid var(--line);
box-shadow: var(--shadow);
position: relative;
overflow: hidden;
}
.top::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(420px 180px at 0% 0%, var(--theme, transparent), transparent 70%);
opacity: 0.18;
pointer-events: none;
}
.top__art {
width: 132px;
height: 132px;
}
.top__body {
display: flex;
flex-direction: column;
justify-content: center;
min-width: 0;
}
.top__kind {
font-size: 0.74rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.top__name {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.6rem;
letter-spacing: -0.02em;
margin: 6px 0 4px;
line-height: 1.15;
}
.top__meta {
color: var(--muted);
font-size: 0.92rem;
}
.top__play {
align-self: flex-start;
margin-top: 16px;
display: inline-flex;
align-items: center;
gap: 9px;
padding: 10px 18px 10px 16px;
border: 0;
border-radius: var(--r-full);
background: var(--theme, var(--accent));
color: #08130c;
font-family: var(--font-body);
font-weight: 700;
font-size: 0.9rem;
cursor: pointer;
transition: transform 0.14s, filter 0.14s;
}
.top__play:hover { transform: scale(1.04); filter: brightness(1.07); }
.top__play:active { transform: scale(0.97); }
.top__play svg { display: block; }
/* ---------- art (cover) ---------- */
.art {
border-radius: var(--r-md);
position: relative;
overflow: hidden;
flex-shrink: 0;
box-shadow: var(--shadow-sm);
background: var(--theme, var(--accent-2));
}
.art::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(70% 90% at 78% 18%, rgba(255, 255, 255, 0.4), transparent 55%),
linear-gradient(150deg, var(--theme, var(--accent-2)), rgba(0, 0, 0, 0.55));
}
.art::after {
content: "";
position: absolute;
inset: -20%;
background:
repeating-linear-gradient(115deg, rgba(255, 255, 255, 0.08) 0 8px, transparent 8px 18px);
mix-blend-mode: overlay;
}
.art.is-round { border-radius: var(--r-full); }
/* ---------- result row ---------- */
.row {
display: grid;
grid-template-columns: 52px 1fr auto;
align-items: center;
gap: 14px;
padding: 9px 12px;
border-radius: var(--r-md);
cursor: pointer;
border: 1px solid transparent;
transition: background 0.14s, border-color 0.14s;
}
.row:hover,
.row.is-active {
background: var(--surface);
border-color: var(--line);
}
.row__art {
width: 52px;
height: 52px;
}
.row__art.is-round { border-radius: var(--r-full); }
.row__body { min-width: 0; }
.row__name {
font-weight: 600;
font-size: 0.96rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row.is-playing .row__name { color: var(--accent); }
.row__meta {
color: var(--muted);
font-size: 0.83rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-top: 2px;
}
.row__right {
display: flex;
align-items: center;
gap: 12px;
color: var(--muted);
font-size: 0.84rem;
font-variant-numeric: tabular-nums;
}
.row__plays {
display: none;
}
.row:hover .row__plays,
.row.is-active .row__plays { display: inline; }
.row__dur {
min-width: 34px;
text-align: right;
}
/* play overlay on art */
.art__play {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: #fff;
opacity: 0;
background: rgba(0, 0, 0, 0.35);
transition: opacity 0.16s;
}
.row:hover .art__play,
.row.is-active .art__play { opacity: 1; }
.art__play svg { filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); }
/* playing equalizer over art */
.art__eq {
position: absolute;
inset: 0;
display: none;
align-items: flex-end;
justify-content: center;
gap: 3px;
padding-bottom: 14px;
background: rgba(0, 0, 0, 0.42);
}
.row.is-playing .art__eq { display: flex; }
.row.is-playing .art__play { display: none; }
.art__eq i {
width: 3px;
height: 30%;
border-radius: 2px;
background: var(--accent);
animation: eq 0.85s ease-in-out infinite;
}
.art__eq i:nth-child(2) { animation-delay: 0.18s; }
.art__eq i:nth-child(3) { animation-delay: 0.36s; }
.art__eq i:nth-child(4) { animation-delay: 0.08s; }
/* ---------- recent searches ---------- */
.recent__head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 12px;
}
.recent__clear {
border: 0;
background: transparent;
color: var(--muted);
font-family: var(--font-body);
font-weight: 600;
font-size: 0.84rem;
cursor: pointer;
padding: 4px 6px;
border-radius: var(--r-sm);
transition: color 0.14s;
}
.recent__clear:hover { color: var(--accent-3); }
.recent__pill {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
text-align: left;
padding: 8px 10px;
border: 0;
background: transparent;
border-radius: var(--r-md);
color: var(--text);
font-family: var(--font-body);
font-size: 0.94rem;
cursor: pointer;
transition: background 0.14s;
}
.recent__pill:hover { background: var(--surface); }
.recent__pill svg { color: var(--muted); flex-shrink: 0; }
.recent__pill span { flex: 1; }
.recent__remove {
border: 0;
background: transparent;
color: var(--muted);
cursor: pointer;
width: 26px;
height: 26px;
border-radius: var(--r-full);
display: grid;
place-items: center;
opacity: 0;
transition: opacity 0.14s, color 0.14s, background 0.14s;
}
.recent__pill:hover .recent__remove { opacity: 1; }
.recent__remove:hover { color: var(--text); background: var(--line-2); }
/* empty / no-results */
.empty {
text-align: center;
padding: 56px 20px;
color: var(--muted);
}
.empty__icon {
display: inline-grid;
place-items: center;
width: 64px;
height: 64px;
border-radius: var(--r-full);
background: var(--surface);
border: 1px solid var(--line);
margin-bottom: 16px;
color: var(--muted);
}
.empty h3 {
font-family: var(--font-display);
color: var(--text);
margin: 0 0 6px;
font-size: 1.1rem;
}
.empty p { margin: 0; font-size: 0.92rem; }
.empty b { color: var(--text); }
/* ---------- toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 24px);
background: var(--surface-2);
border: 1px solid var(--line-2);
color: var(--text);
padding: 11px 18px;
border-radius: var(--r-full);
font-size: 0.9rem;
font-weight: 600;
box-shadow: var(--shadow);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 50;
max-width: calc(100% - 32px);
}
.toast.is-show {
opacity: 1;
transform: translate(-50%, 0);
}
/* keyboard focus */
.row:focus-visible,
.recent__pill:focus-visible,
.top__play:focus-visible,
.search__clear:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
/* ---------- responsive ---------- */
@media (max-width: 520px) {
.app { padding: 22px 14px 70px; }
.top {
grid-template-columns: 96px 1fr;
gap: 14px;
padding: 16px;
}
.top__art { width: 96px; height: 96px; }
.top__name { font-size: 1.25rem; }
.row { grid-template-columns: 48px 1fr auto; gap: 11px; }
.row__art { width: 48px; height: 48px; }
.row__plays { display: none !important; }
.brand__name { font-size: 1.2rem; }
}
@media (prefers-reduced-motion: reduce) {
.eq i,
.art__eq i {
animation: none;
height: 60%;
}
* { transition-duration: 0.001ms !important; }
}
/* Visibility guard: honor the [hidden] attribute over base display */
.search__clear[hidden] {
display: none;
}(function () {
"use strict";
/* ---------- fictional catalog ---------- */
var CATALOG = [
// artists
{ id: "a1", type: "artist", name: "Neon Tides", meta: "Artist", theme: "#1db954", plays: "2.4M monthly", round: true },
{ id: "a2", type: "artist", name: "Velvet Static", meta: "Artist", theme: "#8b5cf6", plays: "1.1M monthly", round: true },
{ id: "a3", type: "artist", name: "Paper Lanterns", meta: "Artist", theme: "#ff3d71", plays: "880K monthly", round: true },
{ id: "a4", type: "artist", name: "Glass Harbor", meta: "Artist", theme: "#38bdf8", plays: "640K monthly", round: true },
{ id: "a5", type: "artist", name: "Saffron Drift", meta: "Artist", theme: "#f59e0b", plays: "1.9M monthly", round: true },
// albums
{ id: "al1", type: "album", name: "Midnight Reservoir", meta: "Album · Neon Tides · 2025", theme: "#1db954", plays: "12.7M" },
{ id: "al2", type: "album", name: "Velvet Static", meta: "Album · Velvet Static · 2024", theme: "#8b5cf6", plays: "8.3M" },
{ id: "al3", type: "album", name: "Low Tide Letters", meta: "Album · Glass Harbor · 2023", theme: "#38bdf8", plays: "5.0M" },
{ id: "al4", type: "album", name: "Saffron & Smoke", meta: "Album · Saffron Drift · 2025", theme: "#f59e0b", plays: "6.6M" },
{ id: "al5", type: "album", name: "Folded Light", meta: "Album · Paper Lanterns · 2024", theme: "#ff3d71", plays: "3.4M" },
// songs
{ id: "s1", type: "song", name: "Paper Lanterns", meta: "Neon Tides", album: "Midnight Reservoir", theme: "#1db954", plays: "48,210,773", dur: "3:42" },
{ id: "s2", type: "song", name: "Reservoir Lights", meta: "Neon Tides", album: "Midnight Reservoir", theme: "#1db954", plays: "31,004,118", dur: "4:05" },
{ id: "s3", type: "song", name: "Static Bloom", meta: "Velvet Static", album: "Velvet Static", theme: "#8b5cf6", plays: "22,887,640", dur: "2:58" },
{ id: "s4", type: "song", name: "Velvet Hours", meta: "Velvet Static", album: "Velvet Static", theme: "#8b5cf6", plays: "19,440,902", dur: "3:21" },
{ id: "s5", type: "song", name: "Harbor Glass", meta: "Glass Harbor", album: "Low Tide Letters", theme: "#38bdf8", plays: "14,002,556", dur: "3:55" },
{ id: "s6", type: "song", name: "Low Tide", meta: "Glass Harbor", album: "Low Tide Letters", theme: "#38bdf8", plays: "11,765,210", dur: "4:12" },
{ id: "s7", type: "song", name: "Saffron Drift", meta: "Saffron Drift", album: "Saffron & Smoke", theme: "#f59e0b", plays: "27,330,180", dur: "3:09" },
{ id: "s8", type: "song", name: "Smoke Signals", meta: "Saffron Drift", album: "Saffron & Smoke", theme: "#f59e0b", plays: "9,118,047", dur: "3:48" },
{ id: "s9", type: "song", name: "Folded Light", meta: "Paper Lanterns", album: "Folded Light", theme: "#ff3d71", plays: "16,520,331", dur: "2:47" },
{ id: "s10", type: "song", name: "Lantern Glow", meta: "Paper Lanterns", album: "Folded Light", theme: "#ff3d71", plays: "7,884,001", dur: "3:33" },
{ id: "s11", type: "song", name: "Tides at Dawn", meta: "Neon Tides", album: "Midnight Reservoir", theme: "#1db954", plays: "5,220,664", dur: "4:30" },
// playlists
{ id: "p1", type: "playlist", name: "Late Night Static", meta: "Playlist · 64 songs", theme: "#8b5cf6", plays: "320K saves" },
{ id: "p2", type: "playlist", name: "Tide Pools", meta: "Playlist · 41 songs", theme: "#38bdf8", plays: "118K saves" },
{ id: "p3", type: "playlist", name: "Saffron Sundown", meta: "Playlist · 80 songs", theme: "#f59e0b", plays: "204K saves" }
];
var GROUPS = [
{ key: "song", title: "Songs", round: false },
{ key: "artist", title: "Artists", round: true },
{ key: "album", title: "Albums", round: false },
{ key: "playlist", title: "Playlists", round: false }
];
var RECENT_KEY = "stealthwave.recent.v1";
var RECENT_MAX = 6;
/* ---------- elements ---------- */
var input = document.getElementById("q");
var clearBtn = document.getElementById("clear");
var chipsBox = document.getElementById("chips");
var results = document.getElementById("results");
var toastEl = document.getElementById("toast");
var state = { query: "", cat: "all", flat: [], active: -1, playingId: null };
var debounceTimer = null;
var toastTimer = null;
/* ---------- helpers ---------- */
function esc(s) {
return s.replace(/[&<>"']/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
});
}
function highlight(text, q) {
var safe = esc(text);
if (!q) return safe;
var i = text.toLowerCase().indexOf(q.toLowerCase());
if (i < 0) return safe;
// re-find on escaped string by mapping the original slice
var before = esc(text.slice(0, i));
var match = esc(text.slice(i, i + q.length));
var after = esc(text.slice(i + q.length));
return before + "<mark>" + match + "</mark>" + after;
}
function matches(item, q) {
var hay = (item.name + " " + (item.meta || "") + " " + (item.album || "")).toLowerCase();
return hay.indexOf(q) >= 0;
}
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-show");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-show");
}, 1900);
}
/* ---------- recent searches ---------- */
function getRecent() {
try {
return JSON.parse(localStorage.getItem(RECENT_KEY)) || [];
} catch (e) {
return [];
}
}
function setRecent(list) {
try {
localStorage.setItem(RECENT_KEY, JSON.stringify(list.slice(0, RECENT_MAX)));
} catch (e) {}
}
function pushRecent(term) {
term = term.trim();
if (!term) return;
var list = getRecent().filter(function (t) {
return t.toLowerCase() !== term.toLowerCase();
});
list.unshift(term);
setRecent(list);
}
/* ---------- art ---------- */
function artMarkup(item, cls) {
var round = item.round ? " is-round" : "";
return (
'<div class="' + cls + round + ' art" style="--theme:' + item.theme + '">' +
'<div class="art__play"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg></div>' +
'<div class="art__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></div>' +
"</div>"
);
}
function rowMarkup(item, q) {
var right = "";
if (item.type === "song") {
right =
'<span class="row__plays">' + item.plays + " plays</span>" +
'<span class="row__dur">' + item.dur + "</span>";
} else {
right = '<span class="row__plays">' + item.plays + "</span>";
}
var meta = item.meta;
if (item.type === "song") meta = highlight(item.meta, q) + " · " + highlight(item.album, q);
else meta = highlight(item.meta, q);
return (
'<div class="row" role="option" tabindex="-1" data-id="' + item.id + '" style="--theme:' + item.theme + '">' +
artMarkup(item, "row__art") +
'<div class="row__body">' +
'<div class="row__name">' + highlight(item.name, q) + "</div>" +
'<div class="row__meta">' + meta + "</div>" +
"</div>" +
'<div class="row__right">' + right + "</div>" +
"</div>"
);
}
/* ---------- render: empty (recent) ---------- */
function renderRecent() {
state.flat = [];
state.active = -1;
var recent = getRecent();
if (!recent.length) {
results.innerHTML =
'<div class="empty">' +
'<div class="empty__icon"><svg viewBox="0 0 24 24" width="28" height="28"><circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2"/><line x1="15.5" y1="15.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></div>' +
"<h3>Search Stealthwave</h3>" +
"<p>Find your favorite artists, albums, songs and playlists.</p>" +
"</div>";
return;
}
var pills = recent
.map(function (term) {
return (
'<button class="recent__pill" type="button" data-term="' + esc(term) + '">' +
'<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="2"/><path d="M12 7v5l3.5 2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>' +
"<span>" + esc(term) + "</span>" +
'<span class="recent__remove" data-remove="' + esc(term) + '" role="button" aria-label="Remove recent search">' +
'<svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true"><line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>' +
"</span>" +
"</button>"
);
})
.join("");
results.innerHTML =
'<div class="group recent">' +
'<div class="recent__head">' +
'<h2 class="group__title">Recent searches</h2>' +
'<button class="recent__clear" type="button" id="recent-clear">Clear all</button>' +
"</div>" +
'<div class="group__list">' + pills + "</div>" +
"</div>";
}
/* ---------- render: results ---------- */
function renderResults() {
var q = state.query.trim().toLowerCase();
if (!q) {
input.setAttribute("aria-expanded", "false");
renderRecent();
return;
}
input.setAttribute("aria-expanded", "true");
var pool = CATALOG.filter(function (item) {
return matches(item, q);
});
if (state.cat !== "all") {
pool = pool.filter(function (item) {
return item.type === state.cat;
});
}
if (!pool.length) {
state.flat = [];
state.active = -1;
results.innerHTML =
'<div class="empty">' +
'<div class="empty__icon"><svg viewBox="0 0 24 24" width="28" height="28"><path d="M9 18V5l12-2v13" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="6" cy="18" r="3" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="16" r="3" fill="none" stroke="currentColor" stroke-width="2"/></svg></div>' +
"<h3>No results for “<b>" + esc(state.query.trim()) + "</b>”</h3>" +
"<p>Try a different artist, album, or song name.</p>" +
"</div>";
return;
}
var html = "";
var flat = [];
// top result (only when "All") — best match by name startsWith, prefer artist/album
if (state.cat === "all") {
var top = pickTop(pool, q);
if (top) {
html +=
'<div class="group">' +
'<div class="top" style="--theme:' + top.theme + '" data-id="' + top.id + '" role="button" tabindex="-1">' +
artMarkupTop(top) +
'<div class="top__body">' +
'<span class="top__kind">' + kindLabel(top.type) + "</span>" +
'<h2 class="top__name">' + highlight(top.name, q) + "</h2>" +
'<span class="top__meta">' + highlight(top.meta, q) + "</span>" +
'<button class="top__play" type="button" data-id="' + top.id + '">' +
'<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true"><path d="M8 5v14l11-7z" fill="currentColor"/></svg>Play</button>' +
"</div></div></div>";
flat.push(top.id);
}
}
GROUPS.forEach(function (g) {
if (state.cat !== "all" && state.cat !== g.key) return;
var items = pool.filter(function (it) {
return it.type === g.key;
});
if (state.cat === "all") items = items.slice(0, g.key === "song" ? 4 : 4);
if (!items.length) return;
var rows = items
.map(function (it) {
flat.push(it.id);
return rowMarkup(it, q);
})
.join("");
html +=
'<div class="group">' +
'<h2 class="group__title">' + g.title + "</h2>" +
'<div class="group__list">' + rows + "</div>" +
"</div>";
});
results.innerHTML = html;
state.flat = flat;
state.active = -1;
restorePlaying();
}
function artMarkupTop(item) {
var round = item.type === "artist" ? " is-round" : "";
return (
'<div class="top__art art' + round + '" style="--theme:' + item.theme + '">' +
'<div class="art__eq" aria-hidden="true"><i></i><i></i><i></i><i></i></div>' +
"</div>"
);
}
function kindLabel(t) {
return { song: "Song", artist: "Artist", album: "Album", playlist: "Playlist" }[t] || t;
}
function pickTop(pool, q) {
var scored = pool
.map(function (it) {
var n = it.name.toLowerCase();
var score = 0;
if (n === q) score = 100;
else if (n.indexOf(q) === 0) score = 70;
else if (n.indexOf(q) > 0) score = 40;
if (it.type === "artist") score += 8;
if (it.type === "album") score += 4;
return { it: it, score: score };
})
.sort(function (a, b) {
return b.score - a.score;
});
return scored.length ? scored[0].it : null;
}
/* ---------- playback (simulated) ---------- */
function restorePlaying() {
if (!state.playingId) return;
var el = results.querySelector('[data-id="' + state.playingId + '"]');
if (el && el.classList.contains("row")) el.classList.add("is-playing");
}
function findItem(id) {
for (var i = 0; i < CATALOG.length; i++) if (CATALOG[i].id === id) return CATALOG[i];
return null;
}
function play(id) {
var item = findItem(id);
if (!item) return;
// clear previous
var prev = results.querySelector(".row.is-playing");
if (prev) prev.classList.remove("is-playing");
state.playingId = id;
var el = results.querySelector('.row[data-id="' + id + '"]');
if (el) el.classList.add("is-playing");
var verb = item.type === "song" ? "Playing" : item.type === "artist" ? "Playing top tracks by" : "Playing";
toast(verb + " " + item.name);
}
/* ---------- keyboard navigation ---------- */
function setActive(idx) {
var nodes = results.querySelectorAll(".row, .top");
nodes.forEach(function (n) {
n.classList.remove("is-active");
});
if (idx < 0 || idx >= state.flat.length) {
state.active = -1;
return;
}
state.active = idx;
var id = state.flat[idx];
var el = results.querySelector('[data-id="' + id + '"]');
if (el) {
el.classList.add("is-active");
el.scrollIntoView({ block: "nearest" });
}
}
/* ---------- events ---------- */
function onInput() {
var val = input.value;
state.query = val;
clearBtn.hidden = val.length === 0;
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function () {
renderResults();
}, 140);
}
input.addEventListener("input", onInput);
input.addEventListener("keydown", function (e) {
if (e.key === "ArrowDown") {
if (!state.flat.length) return;
e.preventDefault();
setActive(Math.min(state.active + 1, state.flat.length - 1));
} else if (e.key === "ArrowUp") {
if (!state.flat.length) return;
e.preventDefault();
setActive(state.active <= 0 ? -1 : state.active - 1);
} else if (e.key === "Enter") {
if (state.active >= 0 && state.flat[state.active]) {
e.preventDefault();
play(state.flat[state.active]);
pushRecent(state.query);
} else if (state.query.trim()) {
pushRecent(state.query);
}
} else if (e.key === "Escape") {
if (input.value) {
input.value = "";
onInput();
}
}
});
clearBtn.addEventListener("click", function () {
input.value = "";
state.query = "";
clearBtn.hidden = true;
input.focus();
renderResults();
});
chipsBox.addEventListener("click", function (e) {
var chip = e.target.closest(".chip");
if (!chip) return;
var cat = chip.getAttribute("data-cat");
state.cat = cat;
chipsBox.querySelectorAll(".chip").forEach(function (c) {
var on = c === chip;
c.classList.toggle("is-active", on);
c.setAttribute("aria-selected", on ? "true" : "false");
});
renderResults();
});
results.addEventListener("click", function (e) {
// remove single recent
var rm = e.target.closest("[data-remove]");
if (rm) {
e.stopPropagation();
var term = rm.getAttribute("data-remove");
setRecent(
getRecent().filter(function (t) {
return t !== term;
})
);
renderRecent();
return;
}
// clear all recent
if (e.target.id === "recent-clear") {
setRecent([]);
renderRecent();
toast("Recent searches cleared");
return;
}
// recent pill -> run search
var pill = e.target.closest(".recent__pill");
if (pill) {
var t = pill.getAttribute("data-term");
input.value = t;
onInput();
input.focus();
return;
}
// play from row / top
var row = e.target.closest(".row, .top, .top__play");
if (row) {
var id = row.getAttribute("data-id");
if (id) {
play(id);
pushRecent(state.query);
}
}
});
/* ---------- init ---------- */
renderRecent();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Music — Search</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=Sora:wght@500;600;700;800&family=Inter:wght@400;500;600;700;800&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="app" role="main">
<header class="app__head">
<div class="brand">
<span class="brand__mark" aria-hidden="true">
<span class="eq"><i></i><i></i><i></i><i></i></span>
</span>
<span class="brand__name">Stealthwave</span>
</div>
<p class="app__sub">Search artists, albums & tracks</p>
</header>
<div class="search" role="search">
<label class="search__field" for="q">
<span class="search__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22">
<circle cx="10.5" cy="10.5" r="6.5" fill="none" stroke="currentColor" stroke-width="2" />
<line x1="15.5" y1="15.5" x2="21" y2="21" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</span>
<input
id="q"
type="text"
class="search__input"
placeholder="What do you want to listen to?"
autocomplete="off"
spellcheck="false"
aria-label="Search music"
aria-expanded="false"
aria-controls="results"
role="combobox"
/>
<button id="clear" class="search__clear" type="button" aria-label="Clear search" hidden>
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<line x1="6" y1="6" x2="18" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<line x1="18" y1="6" x2="6" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
</button>
</label>
<div class="chips" id="chips" role="tablist" aria-label="Filter results by category">
<button class="chip is-active" role="tab" aria-selected="true" data-cat="all" type="button">All</button>
<button class="chip" role="tab" aria-selected="false" data-cat="song" type="button">Songs</button>
<button class="chip" role="tab" aria-selected="false" data-cat="artist" type="button">Artists</button>
<button class="chip" role="tab" aria-selected="false" data-cat="album" type="button">Albums</button>
<button class="chip" role="tab" aria-selected="false" data-cat="playlist" type="button">Playlists</button>
</div>
</div>
<section
id="results"
class="results"
aria-live="polite"
aria-label="Search results"
></section>
</main>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Search (artists · albums · tracks)
A dark-first music search component centered on a prominent rounded input with an animated magnifier that tilts and tints to the accent on focus. As you type, a debounced client-side filter sweeps a fictional catalog and groups the hits into a themed Top result card followed by Songs, Artists, Albums and Playlists sections. Each result carries CSS-drawn album art (no images), and the matched part of every name highlights inline in the accent color.
A chip row — All / Songs / Artists / Albums / Playlists — narrows the result set, swapping the layout
between the grouped overview and a single focused list. Rows reveal a play overlay and play count on
hover, songs show duration timestamps, and artist art is rendered round. Press the arrow keys to walk
through results and Enter to “play”: the active row picks up its cover-pulled accent and an animated
four-bar equalizer, while a toast confirms what’s now playing. When the field is empty, recent searches
appear instead, persisted to localStorage with per-item remove and a clear-all action.
Everything is theme-driven from CSS custom properties, with role="combobox"/role="option" wiring,
aria-selected chips, AA-contrast body text, visible focus rings, a responsive layout down to ~360px,
and a prefers-reduced-motion fallback. No frameworks, no images, and no audio — playback is simulated
with class toggles and timers.
Illustrative UI only — fictional artists, albums, tracks, and data. No real audio playback.