Shop — Search Results
A fast-feeling storefront search results page with a live autocomplete dropdown that blends recent searches, popular queries, and matching products as you type. Keyboard-navigable suggestions, a result count, filterable facet chips, sort control, and product cards with ratings, prices, sale and stock chips. Includes a did-you-mean spelling correction and a friendly no-results state with popular search shortcuts. Built with semantic HTML, an accessible combobox, and zero dependencies.
MCP
Code
/* ===== Stitch & Stone — search results ===== */
:root {
--bg: #ffffff;
--bg-soft: #f6f7fb;
--ink: #16181d;
--muted: #6b7280;
--brand: #3457ff;
--brand-d: #2742d6;
--sale: #e0245e;
--ok: #1f9d55;
--warn: #c2620a;
--line: rgba(16, 18, 29, .1);
--line-2: rgba(16, 18, 29, .06);
--shadow: 0 1px 2px rgba(16, 18, 29, .04), 0 10px 28px rgba(16, 18, 29, .07);
--shadow-lg: 0 22px 60px rgba(16, 18, 29, .18);
--radius: 16px;
--radius-sm: 10px;
--wrap: 1180px;
--tile-a: linear-gradient(135deg, #e7ecff, #c9d6ff);
--tile-b: linear-gradient(135deg, #ffe9d6, #ffd0b3);
--tile-c: linear-gradient(135deg, #d9f3ea, #b6e7d6);
--tile-d: linear-gradient(135deg, #f3e0ff, #e0c6ff);
--tile-e: linear-gradient(135deg, #ffe1ec, #ffc6da);
--tile-f: linear-gradient(135deg, #e3f0ff, #c4ddff);
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
color: var(--ink);
background: var(--bg-soft);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a { color: inherit; }
button { font: inherit; cursor: pointer; }
.skip {
position: absolute;
left: 12px;
top: -48px;
z-index: 50;
background: var(--ink);
color: #fff;
padding: 10px 16px;
border-radius: 10px;
transition: top .15s ease;
text-decoration: none;
}
.skip:focus { top: 12px; }
:focus-visible {
outline: 3px solid color-mix(in srgb, var(--brand) 55%, white);
outline-offset: 2px;
border-radius: 6px;
}
/* ---------- Top bar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 30;
background: rgba(255, 255, 255, .9);
backdrop-filter: saturate(160%) blur(10px);
border-bottom: 1px solid var(--line);
}
.bar-inner {
max-width: var(--wrap);
margin: 0 auto;
padding: 12px 20px;
display: grid;
grid-template-columns: auto 1fr auto;
align-items: center;
gap: 18px;
}
.brand {
display: inline-flex;
align-items: center;
gap: 10px;
text-decoration: none;
font-weight: 800;
letter-spacing: -.01em;
white-space: nowrap;
}
.brand-mark {
display: grid;
place-items: center;
width: 38px;
height: 38px;
border-radius: 12px;
background: linear-gradient(135deg, var(--brand), #6b8bff);
color: #fff;
box-shadow: 0 6px 16px rgba(52, 87, 255, .35);
}
.brand-name { font-size: 17px; }
.searchbox {
display: flex;
gap: 10px;
min-width: 0;
}
.combo {
position: relative;
flex: 1;
min-width: 0;
display: flex;
align-items: center;
background: var(--bg);
border: 1.5px solid var(--line);
border-radius: 14px;
padding: 0 10px 0 12px;
transition: border-color .15s ease, box-shadow .15s ease;
}
.combo:focus-within {
border-color: var(--brand);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--brand) 16%, transparent);
}
.combo-ico { color: var(--muted); flex-shrink: 0; }
.search-input {
flex: 1;
min-width: 0;
border: 0;
outline: 0;
background: transparent;
padding: 12px 8px;
font-size: 15px;
color: var(--ink);
}
.search-input::placeholder { color: var(--muted); }
.clear-btn {
display: grid;
place-items: center;
width: 28px;
height: 28px;
border: 0;
border-radius: 50%;
background: var(--bg-soft);
color: var(--muted);
flex-shrink: 0;
transition: background .15s ease, color .15s ease;
}
.clear-btn:hover { background: #ececf3; color: var(--ink); }
.submit-btn {
border: 0;
border-radius: 14px;
background: var(--brand);
color: #fff;
font-weight: 700;
padding: 0 20px;
white-space: nowrap;
transition: background .15s ease, transform .05s ease;
}
.submit-btn:hover { background: var(--brand-d); }
.submit-btn:active { transform: translateY(1px); }
.bar-actions { display: flex; align-items: center; gap: 6px; }
.icon-btn {
position: relative;
display: grid;
place-items: center;
width: 42px;
height: 42px;
border: 1.5px solid var(--line);
border-radius: 12px;
background: var(--bg);
color: var(--ink);
transition: border-color .15s ease, background .15s ease;
}
.icon-btn:hover { border-color: var(--brand); }
.cart-count {
position: absolute;
top: -6px;
right: -6px;
min-width: 18px;
height: 18px;
padding: 0 5px;
display: grid;
place-items: center;
font-size: 11px;
font-weight: 700;
color: #fff;
background: var(--sale);
border-radius: 999px;
border: 2px solid var(--bg);
}
/* ---------- Autocomplete ---------- */
.autocomplete {
position: absolute;
left: -1.5px;
right: -1.5px;
top: calc(100% + 8px);
margin: 0;
padding: 6px;
list-style: none;
background: var(--bg);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: var(--shadow-lg);
z-index: 40;
max-height: min(70vh, 460px);
overflow-y: auto;
}
.ac-group-label {
padding: 8px 12px 4px;
font-size: 11px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--muted);
}
.ac-item {
display: flex;
align-items: center;
gap: 12px;
padding: 9px 12px;
border-radius: 10px;
cursor: pointer;
scroll-margin: 8px;
}
.ac-item:hover,
.ac-item.is-active {
background: color-mix(in srgb, var(--brand) 9%, transparent);
}
.ac-item.is-active { box-shadow: inset 0 0 0 1.5px color-mix(in srgb, var(--brand) 35%, transparent); }
.ac-thumb {
width: 38px;
height: 38px;
border-radius: 9px;
flex-shrink: 0;
display: grid;
place-items: center;
font-size: 18px;
}
.ac-mini-ico {
width: 30px;
height: 30px;
border-radius: 8px;
flex-shrink: 0;
display: grid;
place-items: center;
background: var(--bg-soft);
color: var(--muted);
}
.ac-body { flex: 1; min-width: 0; }
.ac-label {
font-size: 14px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.ac-label mark {
background: transparent;
color: var(--brand-d);
font-weight: 800;
}
.ac-sub { font-size: 12px; color: var(--muted); }
.ac-price { font-weight: 700; font-size: 13px; white-space: nowrap; }
.ac-arrow { color: var(--muted); flex-shrink: 0; }
/* ---------- Results ---------- */
#main {
max-width: var(--wrap);
margin: 0 auto;
padding: 24px 20px 64px;
}
.results-head { margin-bottom: 18px; }
.results-title {
margin: 8px 0 2px;
font-size: clamp(22px, 3.4vw, 30px);
letter-spacing: -.02em;
font-weight: 800;
}
.term { color: var(--brand); }
.results-meta { margin: 0; color: var(--muted); font-size: 14px; }
.suggest-line {
margin-top: 10px;
font-size: 14px;
color: var(--muted);
}
.dym-btn {
border: 0;
background: transparent;
padding: 0;
color: var(--brand);
font-weight: 700;
text-decoration: underline;
text-underline-offset: 2px;
}
.dym-btn:hover { color: var(--brand-d); }
.toolbar {
margin-top: 18px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
}
.facets { display: flex; gap: 8px; flex-wrap: wrap; }
.chip {
border: 1.5px solid var(--line);
background: var(--bg);
color: var(--ink);
padding: 8px 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
transition: border-color .15s ease, background .15s ease, color .15s ease;
}
.chip:hover { border-color: color-mix(in srgb, var(--brand) 45%, var(--line)); }
.chip.is-active {
background: var(--ink);
border-color: var(--ink);
color: #fff;
}
.chip-toggle[aria-pressed="true"] {
background: color-mix(in srgb, var(--brand) 12%, white);
border-color: var(--brand);
color: var(--brand-d);
}
.sort { display: inline-flex; align-items: center; gap: 8px; }
.sort-label { font-size: 13px; color: var(--muted); font-weight: 600; }
#sortSel {
appearance: none;
border: 1.5px solid var(--line);
background: var(--bg);
color: var(--ink);
padding: 8px 30px 8px 12px;
border-radius: 10px;
font-size: 13px;
font-weight: 600;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%236b7280' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m4 6 4 4 4-4'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 9px center;
}
/* ---------- Grid ---------- */
.grid {
list-style: none;
margin: 22px 0 0;
padding: 0;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
gap: 18px;
}
.card {
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--radius);
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: var(--shadow);
transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
animation: pop .28s ease both;
}
@keyframes pop {
from { opacity: 0; transform: translateY(8px) scale(.98); }
}
.card:hover {
transform: translateY(-3px);
border-color: color-mix(in srgb, var(--brand) 30%, var(--line));
box-shadow: var(--shadow-lg);
}
.card-media {
position: relative;
aspect-ratio: 4 / 3;
display: grid;
place-items: center;
}
.card-media svg { width: 56%; height: 56%; opacity: .92; }
.badge {
position: absolute;
top: 10px;
left: 10px;
font-size: 11px;
font-weight: 800;
letter-spacing: .04em;
text-transform: uppercase;
padding: 4px 9px;
border-radius: 999px;
color: #fff;
}
.badge-sale { background: var(--sale); }
.badge-new { background: var(--ok); }
.wish {
position: absolute;
top: 8px;
right: 8px;
width: 34px;
height: 34px;
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
background: rgba(255, 255, 255, .85);
color: var(--muted);
backdrop-filter: blur(4px);
transition: color .15s ease, transform .1s ease;
}
.wish:hover { color: var(--sale); }
.wish[aria-pressed="true"] { color: var(--sale); }
.wish[aria-pressed="true"] svg { fill: currentColor; }
.wish:active { transform: scale(.9); }
.card-body { padding: 14px 14px 16px; display: flex; flex-direction: column; gap: 6px; flex: 1; }
.card-cat { font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); }
.card-name { margin: 0; font-size: 15px; font-weight: 600; line-height: 1.35; }
.rating { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
.stars { color: #f5a623; letter-spacing: 1px; font-size: 13px; }
.price-row { margin-top: auto; display: flex; align-items: baseline; gap: 8px; padding-top: 6px; }
.price { font-size: 18px; font-weight: 800; letter-spacing: -.01em; }
.price-was { font-size: 13px; color: var(--muted); text-decoration: line-through; }
.stock {
font-size: 11px;
font-weight: 700;
padding: 3px 8px;
border-radius: 999px;
}
.stock-in { color: var(--ok); background: color-mix(in srgb, var(--ok) 12%, white); }
.stock-low { color: var(--warn); background: color-mix(in srgb, var(--warn) 14%, white); }
.stock-out { color: var(--muted); background: var(--bg-soft); }
.add-btn {
margin-top: 10px;
width: 100%;
border: 0;
border-radius: 11px;
background: var(--ink);
color: #fff;
font-weight: 700;
padding: 11px;
transition: background .15s ease, transform .05s ease;
}
.add-btn:hover { background: #000; }
.add-btn:active { transform: translateY(1px); }
.add-btn:disabled { background: var(--line); color: var(--muted); cursor: not-allowed; }
/* ---------- Empty state ---------- */
.empty {
text-align: center;
padding: 56px 20px;
max-width: 460px;
margin: 30px auto;
}
.empty-art {
width: 86px;
height: 86px;
margin: 0 auto 18px;
display: grid;
place-items: center;
border-radius: 24px;
background: var(--tile-a);
color: var(--brand-d);
}
.empty-title { margin: 0 0 6px; font-size: 22px; font-weight: 800; }
.empty-title span { color: var(--brand); }
.empty-sub { margin: 0 0 18px; color: var(--muted); }
.empty-tags { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
.empty-tag {
border: 1.5px solid var(--line);
background: var(--bg);
color: var(--ink);
padding: 8px 14px;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
transition: border-color .15s ease, color .15s ease;
}
.empty-tag:hover { border-color: var(--brand); color: var(--brand-d); }
/* ---------- Trust strip ---------- */
.trust {
max-width: var(--wrap);
margin: 8px auto 0;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px;
padding-top: 24px;
border-top: 1px solid var(--line);
}
.trust-item {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 0;
}
.trust-ico { font-size: 22px; }
.trust-item strong { display: block; font-size: 14px; }
.trust-item span { font-size: 12px; color: var(--muted); }
/* ---------- Toast ---------- */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%) translateY(20px);
background: var(--ink);
color: #fff;
padding: 12px 18px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
box-shadow: var(--shadow-lg);
opacity: 0;
pointer-events: none;
transition: opacity .2s ease, transform .2s ease;
z-index: 60;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
/* ---------- Responsive ---------- */
@media (max-width: 860px) {
.bar-inner {
grid-template-columns: 1fr auto;
grid-template-areas: "brand actions" "search search";
gap: 12px;
}
.brand { grid-area: brand; }
.bar-actions { grid-area: actions; }
.searchbox { grid-area: search; }
.trust { grid-template-columns: 1fr; gap: 8px; }
}
@media (max-width: 560px) {
.bar-inner { padding: 10px 14px; }
#main { padding: 18px 14px 56px; }
.grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px; }
.submit-btn { padding: 0 14px; }
.brand-name { display: none; }
.toolbar { gap: 10px; }
.ac-sub { display: none; }
}
@media (max-width: 380px) {
.grid { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}/* ===== Stitch & Stone — search results ===== */
(function () {
"use strict";
/* ---------- Inline SVG product silhouettes ---------- */
var ICONS = {
shoe: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M2 17h18a2 2 0 0 0 2-2c0-2-2-2.5-4-3.5L13 8l-2 2-2-1-2 2H4a2 2 0 0 0-2 2z"/><path d="M2 14h20"/></svg>',
jacket: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3 4 5v6l2 1v9h12v-9l2-1V5l-5-2-3 3z"/><path d="M12 6v15"/></svg>',
lamp: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3h8l3 7H5z"/><path d="M12 10v8"/><path d="M8 21h8"/></svg>',
bag: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 8h12l1 12H5z"/><path d="M9 8a3 3 0 0 1 6 0"/></svg>',
bottle: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M10 2h4v3l1 2v13a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2V7l1-2z"/><path d="M9 11h6"/></svg>',
watch: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 9v3l2 1"/><path d="M9 7 8 3h8l-1 4M9 17l-1 4h8l-1-4"/></svg>',
cap: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14a8 8 0 0 1 16 0z"/><path d="M12 6a8 8 0 0 0-8 8h8z"/><path d="M4 14h18"/></svg>',
chair: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3v8h12V3M6 11l-1 10M18 11l1 10M5 16h14"/></svg>'
};
var TILES = ["--tile-a", "--tile-b", "--tile-c", "--tile-d", "--tile-e", "--tile-f"];
/* ---------- Catalog (fictional) ---------- */
var PRODUCTS = [
{ id: 1, name: "Trailblazer Running Shoe", cat: "Footwear", icon: "shoe", tile: 0, price: 119, was: 149, rating: 4.7, reviews: 412, stock: "in", sale: true, kw: "run running shoe sneaker trail" },
{ id: 2, name: "Featherlite Race Trainers", cat: "Footwear", icon: "shoe", tile: 5, price: 138, was: 0, rating: 4.5, reviews: 207, stock: "low", sale: false, kw: "run running race trainer light" },
{ id: 3, name: "Summit Trail Sneaker", cat: "Footwear", icon: "shoe", tile: 2, price: 96, was: 0, rating: 4.2, reviews: 88, stock: "in", sale: false, kw: "run trail sneaker hiking shoe" },
{ id: 4, name: "City Runner Knit Low", cat: "Footwear", icon: "shoe", tile: 1, price: 74, was: 99, rating: 4.0, reviews: 154, stock: "in", sale: true, kw: "run runner knit shoe city" },
{ id: 5, name: "Stormshell Rain Jacket", cat: "Apparel", icon: "jacket", tile: 3, price: 159, was: 0, rating: 4.8, reviews: 321, stock: "in", sale: false, kw: "jacket rain run coat waterproof" },
{ id: 6, name: "Pace Wind Vest", cat: "Apparel", icon: "jacket", tile: 4, price: 64, was: 89, rating: 4.4, reviews: 73, stock: "low", sale: true, kw: "vest jacket run wind apparel" },
{ id: 7, name: "Daybreak Running Cap", cat: "Apparel", icon: "cap", tile: 2, price: 28, was: 0, rating: 4.6, reviews: 199, stock: "in", sale: false, kw: "cap hat run running apparel" },
{ id: 8, name: "Horizon Hydration Pack", cat: "Gear", icon: "bag", tile: 5, price: 89, was: 0, rating: 4.3, reviews: 61, stock: "out", sale: false, kw: "pack bag run hydration gear backpack" },
{ id: 9, name: "Pulse Sport Watch", cat: "Gear", icon: "watch", tile: 0, price: 199, was: 249, rating: 4.9, reviews: 540, stock: "in", sale: true, kw: "watch run running gear gps fitness" },
{ id: 10, name: "Trailflask Insulated Bottle", cat: "Gear", icon: "bottle", tile: 2, price: 32, was: 0, rating: 4.1, reviews: 142, stock: "in", sale: false, kw: "bottle run water gear flask hydration" },
{ id: 11, name: "Aurora Desk Lamp", cat: "Gear", icon: "lamp", tile: 3, price: 78, was: 0, rating: 4.5, reviews: 95, stock: "low", sale: false, kw: "lamp light desk home gear" },
{ id: 12, name: "Loft Lounge Chair", cat: "Gear", icon: "chair", tile: 4, price: 349, was: 449, rating: 4.7, reviews: 38, stock: "in", sale: true, kw: "chair furniture home lounge gear seat" }
];
var RECENT = ["running shoes", "rain jacket", "sport watch"];
var POPULAR = ["sneakers", "lamp", "hydration pack", "wind vest"];
var DICT = ["running", "shoe", "sneaker", "jacket", "vest", "watch", "lamp", "bottle", "chair", "trail", "hydration", "cap"];
var money = function (n) { return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 }); };
var esc = function (s) { return s.replace(/[&<>"]/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; }); };
var stars = function (r) {
var full = Math.round(r);
return "★★★★★".slice(0, full) + "☆☆☆☆☆".slice(0, 5 - full);
};
/* ---------- DOM refs ---------- */
var form = document.getElementById("searchForm");
var input = document.getElementById("q");
var combo = input.closest(".combo");
var acList = document.getElementById("acList");
var clearBtn = document.getElementById("clearBtn");
var grid = document.getElementById("results");
var emptyEl = document.getElementById("empty");
var emptyTerm = document.getElementById("emptyTerm");
var emptyTags = document.getElementById("emptyTags");
var termOut = document.getElementById("termOut");
var resultMeta = document.getElementById("resultMeta");
var dymWrap = document.getElementById("didYouMean");
var dymBtn = document.getElementById("dymBtn");
var facetsEl = document.getElementById("facets");
var sortSel = document.getElementById("sortSel");
var cartCountEl = document.getElementById("cartCount");
var toastEl = document.getElementById("toast");
var state = { query: "run", category: "all", onSale: false, inStock: false, sort: "relevance" };
var cart = 2;
var acItems = [];
var acIndex = -1;
var toastTimer;
/* ---------- Toast ---------- */
function toast(msg) {
toastEl.textContent = msg;
toastEl.hidden = false;
requestAnimationFrame(function () { toastEl.classList.add("show"); });
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("show");
setTimeout(function () { toastEl.hidden = true; }, 220);
}, 1900);
}
/* ---------- Matching / scoring ---------- */
function matches(p, q) {
if (!q) return true;
var hay = (p.name + " " + p.cat + " " + p.kw).toLowerCase();
return q.toLowerCase().split(/\s+/).every(function (t) { return hay.indexOf(t) !== -1; });
}
function score(p, q) {
var hay = (p.name + " " + p.kw).toLowerCase();
var s = 0;
q.toLowerCase().split(/\s+/).forEach(function (t) {
if (p.name.toLowerCase().indexOf(t) === 0) s += 5;
else if (p.name.toLowerCase().indexOf(t) !== -1) s += 3;
else if (hay.indexOf(t) !== -1) s += 1;
});
return s;
}
function getResults() {
var q = state.query.trim();
var list = PRODUCTS.filter(function (p) {
if (!matches(p, q)) return false;
if (state.category !== "all" && p.cat !== state.category) return false;
if (state.onSale && !p.sale) return false;
if (state.inStock && p.stock === "out") return false;
return true;
});
if (state.sort === "low") list.sort(function (a, b) { return a.price - b.price; });
else if (state.sort === "high") list.sort(function (a, b) { return b.price - a.price; });
else if (state.sort === "rating") list.sort(function (a, b) { return b.rating - a.rating; });
else list.sort(function (a, b) { return score(b, q) - score(a, q); });
return list;
}
/* ---------- Levenshtein for did-you-mean ---------- */
function lev(a, b) {
var m = a.length, n = b.length, d = [], i, j;
for (i = 0; i <= m; i++) d[i] = [i];
for (j = 0; j <= n; j++) d[0][j] = j;
for (i = 1; i <= m; i++) for (j = 1; j <= n; j++) {
d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
}
return d[m][n];
}
function suggestSpelling(q) {
var w = q.trim().toLowerCase();
if (!w || w.indexOf(" ") !== -1 || w.length < 3) return null;
var best = null, bestD = 99;
DICT.forEach(function (d) {
var dist = lev(w, d);
if (dist > 0 && dist < bestD && dist <= 2) { bestD = dist; best = d; }
});
return best;
}
/* ---------- Render grid ---------- */
function tileVar(p) { return "var(" + TILES[p.tile] + ")"; }
function stockChip(stock) {
if (stock === "in") return '<span class="stock stock-in">In stock</span>';
if (stock === "low") return '<span class="stock stock-low">Low stock</span>';
return '<span class="stock stock-out">Sold out</span>';
}
function renderResults() {
var list = getResults();
var q = state.query.trim();
termOut.textContent = "“" + (q || "everything") + "”";
// did-you-mean
var fix = null;
if (q && list.length === 0) fix = suggestSpelling(q);
if (fix) {
dymBtn.textContent = fix;
dymWrap.hidden = false;
} else {
dymWrap.hidden = true;
}
if (list.length === 0) {
grid.innerHTML = "";
emptyEl.hidden = false;
emptyTerm.textContent = "“" + (q || "your filters") + "”";
resultMeta.textContent = "0 products";
return;
}
emptyEl.hidden = true;
resultMeta.textContent = list.length + (list.length === 1 ? " product" : " products");
grid.innerHTML = list.map(function (p) {
var badge = p.sale
? '<span class="badge badge-sale">Sale</span>'
: (p.reviews < 80 ? '<span class="badge badge-new">New</span>' : "");
var wasHtml = p.was ? '<span class="price-was">' + money(p.was) + "</span>" : "";
var out = p.stock === "out";
return '' +
'<li class="card">' +
'<div class="card-media" style="background:' + tileVar(p) + '">' +
badge +
'<button class="wish" type="button" aria-pressed="false" aria-label="Save ' + esc(p.name) + ' to wishlist" data-wish="' + p.id + '">' +
'<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 1 0-7.8 7.8L12 21l8.8-8.6a5.5 5.5 0 0 0 0-7.8z"/></svg>' +
'</button>' +
ICONS[p.icon] +
'</div>' +
'<div class="card-body">' +
'<span class="card-cat">' + esc(p.cat) + '</span>' +
'<h3 class="card-name">' + esc(p.name) + '</h3>' +
'<div class="rating"><span class="stars" aria-hidden="true">' + stars(p.rating) + '</span>' +
'<span>' + p.rating.toFixed(1) + ' (' + p.reviews + ')</span></div>' +
'<div class="price-row">' +
'<span class="price">' + money(p.price) + '</span>' + wasHtml +
stockChip(p.stock) +
'</div>' +
'<button class="add-btn" type="button" data-add="' + p.id + '"' + (out ? " disabled" : "") + '>' +
(out ? "Notify me" : "Add to cart") + '</button>' +
'</div>' +
'</li>';
}).join("");
}
/* ---------- Autocomplete ---------- */
function buildSuggestions(q) {
q = q.trim().toLowerCase();
var groups = [];
if (!q) {
if (RECENT.length) groups.push({ label: "Recent", items: RECENT.map(function (t) { return { type: "term", text: t, icon: "clock" }; }) });
groups.push({ label: "Popular", items: POPULAR.map(function (t) { return { type: "term", text: t, icon: "trend" }; }) });
return groups;
}
var termHits = RECENT.concat(POPULAR).filter(function (t, i, arr) {
return t.toLowerCase().indexOf(q) !== -1 && arr.indexOf(t) === i;
}).slice(0, 3).map(function (t, i) {
return { type: "term", text: t, icon: RECENT.indexOf(t) !== -1 ? "clock" : "trend" };
});
if (termHits.length) groups.push({ label: "Suggestions", items: termHits });
var prodHits = PRODUCTS.filter(function (p) { return matches(p, q); })
.sort(function (a, b) { return score(b, q) - score(a, q); })
.slice(0, 5)
.map(function (p) { return { type: "product", product: p }; });
if (prodHits.length) groups.push({ label: "Products", items: prodHits });
return groups;
}
function highlight(text, q) {
q = q.trim();
if (!q) return esc(text);
var idx = text.toLowerCase().indexOf(q.toLowerCase());
if (idx === -1) return esc(text);
return esc(text.slice(0, idx)) + "<mark>" + esc(text.slice(idx, idx + q.length)) + "</mark>" + esc(text.slice(idx + q.length));
}
var MINI = {
clock: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>',
trend: '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 17 6-6 4 4 8-8"/><path d="M17 7h4v4"/></svg>'
};
function openAC() {
var q = input.value;
var groups = buildSuggestions(q);
acList.innerHTML = "";
acItems = [];
if (!groups.length) { closeAC(); return; }
groups.forEach(function (g) {
var lbl = document.createElement("li");
lbl.className = "ac-group-label";
lbl.setAttribute("aria-hidden", "true");
lbl.textContent = g.label;
acList.appendChild(lbl);
g.items.forEach(function (it) {
var li = document.createElement("li");
li.className = "ac-item";
li.setAttribute("role", "option");
li.setAttribute("aria-selected", "false");
li.id = "ac-opt-" + acItems.length;
if (it.type === "term") {
li.dataset.value = it.text;
li.innerHTML =
'<span class="ac-mini-ico" aria-hidden="true">' + MINI[it.icon] + '</span>' +
'<span class="ac-body"><span class="ac-label">' + highlight(it.text, q) + '</span></span>' +
'<svg class="ac-arrow" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 17 17 7M7 7h10v10"/></svg>';
} else {
var p = it.product;
li.dataset.value = p.name;
li.dataset.productId = p.id;
li.innerHTML =
'<span class="ac-thumb" style="background:var(' + TILES[p.tile] + ')" aria-hidden="true">' +
'<span style="width:22px;height:22px;display:grid;place-items:center;color:rgba(22,24,29,.75)">' + ICONS[p.icon] + '</span>' +
'</span>' +
'<span class="ac-body"><span class="ac-label">' + highlight(p.name, q) + '</span>' +
'<span class="ac-sub">' + esc(p.cat) + ' · ' + p.rating.toFixed(1) + ' ★</span></span>' +
'<span class="ac-price">' + money(p.price) + '</span>';
}
acList.appendChild(li);
acItems.push(li);
});
});
acList.hidden = false;
combo.setAttribute("aria-expanded", "true");
acIndex = -1;
input.setAttribute("aria-activedescendant", "");
}
function closeAC() {
acList.hidden = true;
acList.innerHTML = "";
combo.setAttribute("aria-expanded", "false");
acItems = [];
acIndex = -1;
input.setAttribute("aria-activedescendant", "");
}
function setActive(i) {
acItems.forEach(function (el, idx) {
var on = idx === i;
el.classList.toggle("is-active", on);
el.setAttribute("aria-selected", on ? "true" : "false");
});
acIndex = i;
if (i >= 0 && acItems[i]) {
input.setAttribute("aria-activedescendant", acItems[i].id);
acItems[i].scrollIntoView({ block: "nearest" });
} else {
input.setAttribute("aria-activedescendant", "");
}
}
function chooseItem(el) {
if (el.dataset.productId) {
var p = PRODUCTS.find(function (x) { return x.id === +el.dataset.productId; });
input.value = p.name;
runSearch(p.name);
toast("Showing " + p.name);
} else {
input.value = el.dataset.value;
runSearch(el.dataset.value);
}
closeAC();
}
/* ---------- Search execution ---------- */
function runSearch(q) {
state.query = q;
toggleClear();
renderResults();
}
function toggleClear() {
clearBtn.hidden = input.value.length === 0;
}
/* ---------- Events ---------- */
var debounce;
input.addEventListener("input", function () {
toggleClear();
clearTimeout(debounce);
debounce = setTimeout(openAC, 90);
});
input.addEventListener("focus", function () {
if (acList.hidden) openAC();
});
input.addEventListener("keydown", function (e) {
var open = !acList.hidden && acItems.length;
if (e.key === "ArrowDown") {
e.preventDefault();
if (!open) { openAC(); return; }
setActive(acIndex >= acItems.length - 1 ? 0 : acIndex + 1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (!open) return;
setActive(acIndex <= 0 ? acItems.length - 1 : acIndex - 1);
} else if (e.key === "Enter") {
if (open && acIndex >= 0) {
e.preventDefault();
chooseItem(acItems[acIndex]);
}
} else if (e.key === "Escape") {
if (!acList.hidden) { e.preventDefault(); closeAC(); }
}
});
acList.addEventListener("mousemove", function (e) {
var li = e.target.closest(".ac-item");
if (li) setActive(acItems.indexOf(li));
});
acList.addEventListener("mousedown", function (e) {
var li = e.target.closest(".ac-item");
if (li) { e.preventDefault(); chooseItem(li); }
});
form.addEventListener("submit", function (e) {
e.preventDefault();
closeAC();
runSearch(input.value.trim());
input.blur();
});
clearBtn.addEventListener("click", function () {
input.value = "";
state.query = "";
toggleClear();
closeAC();
renderResults();
input.focus();
});
document.addEventListener("click", function (e) {
if (!combo.contains(e.target)) closeAC();
});
// facets
facetsEl.addEventListener("click", function (e) {
var btn = e.target.closest(".chip");
if (!btn) return;
var f = btn.dataset.facet;
if (f === "sale") {
state.onSale = !state.onSale;
btn.setAttribute("aria-pressed", String(state.onSale));
} else if (f === "instock") {
state.inStock = !state.inStock;
btn.setAttribute("aria-pressed", String(state.inStock));
} else {
state.category = f;
facetsEl.querySelectorAll("[data-facet]").forEach(function (c) {
if (c.classList.contains("chip-toggle")) return;
var on = c === btn;
c.classList.toggle("is-active", on);
c.setAttribute("aria-pressed", String(on));
});
}
renderResults();
});
// sort
sortSel.addEventListener("change", function () {
state.sort = sortSel.value;
renderResults();
});
// did you mean
dymBtn.addEventListener("click", function () {
var v = dymBtn.textContent;
input.value = v;
toggleClear();
runSearch(v);
});
// empty-state popular tags
emptyTags.innerHTML = POPULAR.map(function (t) {
return '<button class="empty-tag" type="button" data-tag="' + esc(t) + '">' + esc(t) + "</button>";
}).join("");
emptyTags.addEventListener("click", function (e) {
var b = e.target.closest(".empty-tag");
if (!b) return;
input.value = b.dataset.tag;
toggleClear();
runSearch(b.dataset.tag);
});
// grid actions (delegated)
grid.addEventListener("click", function (e) {
var add = e.target.closest("[data-add]");
if (add) {
var p = PRODUCTS.find(function (x) { return x.id === +add.dataset.add; });
cart += 1;
cartCountEl.textContent = cart;
add.textContent = "Added ✓";
setTimeout(function () { add.textContent = "Add to cart"; }, 1100);
toast(p.name + " added to cart");
return;
}
var wish = e.target.closest("[data-wish]");
if (wish) {
var on = wish.getAttribute("aria-pressed") === "true";
wish.setAttribute("aria-pressed", String(!on));
toast(on ? "Removed from wishlist" : "Saved to wishlist");
}
});
/* ---------- Init ---------- */
toggleClear();
renderResults();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stitch & Stone — Search Results</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="topbar" role="banner">
<div class="bar-inner">
<a class="brand" href="#" aria-label="Stitch and Stone home">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><path d="M3 6h18"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>
</span>
<span class="brand-name">Stitch & Stone</span>
</a>
<form class="searchbox" id="searchForm" role="search" autocomplete="off">
<div class="combo" role="combobox" aria-expanded="false" aria-owns="acList" aria-haspopup="listbox">
<svg class="combo-ico" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>
<input
id="q"
class="search-input"
type="text"
name="q"
placeholder="Search jackets, lamps, sneakers…"
aria-label="Search the store"
aria-autocomplete="list"
aria-controls="acList"
aria-activedescendant=""
value="run" />
<button type="button" class="clear-btn" id="clearBtn" aria-label="Clear search" hidden>
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><path d="M6 6l12 12M18 6 6 18"/></svg>
</button>
<ul class="autocomplete" id="acList" role="listbox" aria-label="Search suggestions" hidden></ul>
</div>
<button type="submit" class="submit-btn">Search</button>
</form>
<div class="bar-actions">
<button class="icon-btn" id="cartBtn" aria-label="Cart, 2 items">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6"/></svg>
<span class="cart-count" id="cartCount">2</span>
</button>
</div>
</div>
</header>
<main id="main">
<section class="results-wrap" aria-labelledby="resultsHeading">
<div class="results-head">
<h1 id="resultsHeading" class="results-title">
Results for <span class="term" id="termOut">“run”</span>
</h1>
<p class="results-meta" id="resultMeta" aria-live="polite">12 products</p>
<div class="suggest-line" id="didYouMean" hidden>
Did you mean <button type="button" class="dym-btn" id="dymBtn"></button>?
</div>
<div class="toolbar">
<div class="facets" id="facets" role="group" aria-label="Filter results">
<button class="chip is-active" data-facet="all" aria-pressed="true">All</button>
<button class="chip" data-facet="Footwear" aria-pressed="false">Footwear</button>
<button class="chip" data-facet="Apparel" aria-pressed="false">Apparel</button>
<button class="chip" data-facet="Gear" aria-pressed="false">Gear</button>
<button class="chip chip-toggle" data-facet="sale" aria-pressed="false">On sale</button>
<button class="chip chip-toggle" data-facet="instock" aria-pressed="false">In stock</button>
</div>
<label class="sort">
<span class="sort-label">Sort</span>
<select id="sortSel" aria-label="Sort results">
<option value="relevance">Relevance</option>
<option value="low">Price: low to high</option>
<option value="high">Price: high to low</option>
<option value="rating">Top rated</option>
</select>
</label>
</div>
</div>
<ul class="grid" id="results" role="list" aria-live="polite"></ul>
<div class="empty" id="empty" hidden>
<div class="empty-art" aria-hidden="true">
<svg viewBox="0 0 24 24" width="46" height="46" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/><path d="M8 11h6"/></svg>
</div>
<h2 class="empty-title">No matches for <span id="emptyTerm"></span></h2>
<p class="empty-sub">Check the spelling or try one of these popular searches.</p>
<div class="empty-tags" id="emptyTags"></div>
</div>
</section>
<aside class="trust" aria-label="Why shop with us">
<div class="trust-item"><span class="trust-ico" aria-hidden="true">🚚</span><div><strong>Free shipping</strong><span>On orders over $50</span></div></div>
<div class="trust-item"><span class="trust-ico" aria-hidden="true">🔒</span><div><strong>Secure checkout</strong><span>256-bit encrypted</span></div></div>
<div class="trust-item"><span class="trust-ico" aria-hidden="true">↩️</span><div><strong>30-day returns</strong><span>No-fuss refunds</span></div></div>
</aside>
</main>
<div class="toast" id="toast" role="status" aria-live="polite" hidden></div>
<script src="script.js"></script>
</body>
</html>Search Results
A complete storefront search experience built around an accessible combobox. As you type, a live dropdown groups recent searches, popular queries, and matching products (with thumbnail, category, rating, and price), highlighting the matched substring inline. The list is fully keyboard navigable with arrow keys, Enter to select, and Escape to dismiss, and it wires up aria-activedescendant for screen readers.
Submitting or picking a suggestion filters the results grid instantly. Facet chips switch category or toggle On sale / In stock, a sort control orders by relevance, price, or rating, and the live result count updates as you go. Each product card shows star ratings, review counts, original-vs-sale pricing, and a stock chip, with working Add-to-cart and wishlist buttons plus a toast confirmation.
When a query returns nothing, a did-you-mean correction (powered by an inline edit-distance check) offers the closest term, and a polished empty state suggests popular searches as one-tap shortcuts. Everything is plain HTML, CSS, and vanilla JS — no frameworks, no external images, and responsive down to about 360px.
Illustrative storefront UI only — fictional products, prices, and reviews. No real checkout.