Predictive Search
Instant-as-you-type search dropdown. Replaces Searchanise / Boost AI.
Live preview
See it in action.
Fully interactive, drag, click, scroll inside the frame, toggle to mobile.
About this section
A modal-style predictive search overlay powered by Shopify's native /search/suggest.json endpoint, no third-party search index, no monthly fee. Shows products with images and prices, collections, and pages in grouped result panels. Recent-searches memory in localStorage, popular-terms quick chips, full keyboard navigation (arrows, Enter, Escape, Cmd+K to open), highlighted matching text, mobile full-screen layout.
Install in 90 seconds
- 01
Create /sections/modblo-predictive-search.liquid.
- 02
Paste the section code and save.
- 03
Add the section to your header.liquid (replace your existing search icon/button).
- 04
(Optional) Configure popular search terms in the section settings — these show as quick chips when the search is empty.
- 05
(Optional) Cmd/Ctrl + K opens the search overlay from anywhere on the site, document this in your help docs.
The Liquid
{%- comment -%}
modblo. Predictive Search
Replaces Searchanise / Boost AI Search / Algolia widgets.
Instant-as-you-type results powered by Shopify's native
/search/suggest.json endpoint, no third-party search index needed.
Shows products (with images, title, price), collections, and pages in
grouped result panels. Keyboard nav, recent-searches memory.
{%- endcomment -%}
<div class="modblo-ps" data-modblo-ps
data-section-id="{{ section.id }}"
data-mode="{{ section.settings.mode | default: 'overlay' }}"
data-types="{{ section.settings.result_types | default: 'product,collection,page' }}"
style="--modblo-ps-bg: {{ section.settings.bg }};
--modblo-ps-fg: {{ section.settings.fg }};
--modblo-ps-accent: {{ section.settings.accent }};">
{%- comment -%} Trigger: a search button that opens the overlay {%- endcomment -%}
<button type="button" class="modblo-ps__trigger" data-modblo-ps-open aria-label="Open search">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.6" 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>
{%- if section.settings.show_label -%}
<span class="modblo-ps__trigger-label">{{ section.settings.trigger_label | default: 'Search' }}</span>
{%- endif -%}
</button>
{%- comment -%} Overlay panel {%- endcomment -%}
<div class="modblo-ps__overlay" data-modblo-ps-overlay role="dialog" aria-modal="true" aria-label="Search">
<div class="modblo-ps__panel">
<header class="modblo-ps__header">
<span class="modblo-ps__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" 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"/>
</svg>
</span>
<input
type="search"
class="modblo-ps__input"
placeholder="{{ section.settings.input_placeholder | default: 'Search products, collections, articles' }}"
autocomplete="off"
spellcheck="false"
data-modblo-ps-input
aria-label="Search">
<button type="button" class="modblo-ps__close" data-modblo-ps-close aria-label="Close search">Esc</button>
</header>
<div class="modblo-ps__body" data-modblo-ps-body>
{%- comment -%} Idle state: recent searches + popular {%- endcomment -%}
<div class="modblo-ps__idle" data-modblo-ps-idle>
<div class="modblo-ps__idle-section" data-modblo-ps-recent hidden>
<p class="modblo-ps__idle-label">Recent</p>
<ul class="modblo-ps__chips" data-modblo-ps-recent-list></ul>
</div>
{%- if section.settings.popular_terms != blank -%}
<div class="modblo-ps__idle-section">
<p class="modblo-ps__idle-label">Popular searches</p>
<ul class="modblo-ps__chips">
{%- assign popular = section.settings.popular_terms | split: ',' -%}
{%- for term in popular -%}
{%- assign t = term | strip -%}
<li><a href="/search?q={{ t | url_encode }}" class="modblo-ps__chip" data-modblo-ps-popular="{{ t | escape }}">{{ t }}</a></li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
</div>
{%- comment -%} Loading state {%- endcomment -%}
<div class="modblo-ps__loading" data-modblo-ps-loading hidden>
<span class="modblo-ps__spinner" aria-hidden="true"></span>
Searching...
</div>
{%- comment -%} Results state — populated by JS {%- endcomment -%}
<div class="modblo-ps__results" data-modblo-ps-results hidden></div>
{%- comment -%} Empty state {%- endcomment -%}
<div class="modblo-ps__empty" data-modblo-ps-empty hidden>
<p class="modblo-ps__empty-h">No results for "<span data-modblo-ps-empty-q></span>"</p>
<p class="modblo-ps__empty-sub">Try a different search term or browse our <a href="/collections/all">full catalog</a>.</p>
</div>
</div>
<footer class="modblo-ps__footer">
<a href="/search" class="modblo-ps__view-all" data-modblo-ps-view-all>
View all search results <span aria-hidden="true">→</span>
</a>
</footer>
</div>
</div>
</div>
<style>
.modblo-ps__trigger {
display: inline-flex; align-items: center; gap: 8px;
background: color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 5%, transparent);
color: var(--modblo-ps-fg, #0b0b0c);
border: 1px solid color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 12%, transparent);
border-radius: 999px;
padding: 8px 14px; font-size: 13px; font-weight: 500;
cursor: pointer;
transition: background .2s, border-color .2s;
}
.modblo-ps__trigger:hover { border-color: color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 25%, transparent); }
.modblo-ps__trigger-label { font-size: 13px; }
.modblo-ps__overlay {
position: fixed; inset: 0; z-index: 90;
background: rgba(0,0,0,.55);
opacity: 0; visibility: hidden; pointer-events: none;
transition: opacity .25s ease, visibility .25s;
display: flex; align-items: flex-start; justify-content: center;
padding: 80px 24px 24px;
}
.modblo-ps.is-open .modblo-ps__overlay { opacity: 1; visibility: visible; pointer-events: auto; }
.modblo-ps__panel {
width: min(640px, 100%);
max-height: calc(100vh - 100px);
background: var(--modblo-ps-bg, #fff); color: var(--modblo-ps-fg, #0b0b0c);
border-radius: 16px;
box-shadow: 0 32px 80px -24px rgba(0,0,0,.45);
transform: translateY(-12px);
transition: transform .35s cubic-bezier(.16,1,.3,1);
display: flex; flex-direction: column; overflow: hidden;
}
.modblo-ps.is-open .modblo-ps__panel { transform: translateY(0); }
.modblo-ps__header {
display: flex; align-items: center; gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 8%, transparent);
}
.modblo-ps__icon { opacity: .55; }
.modblo-ps__input {
flex: 1; background: transparent; color: inherit;
border: 0; padding: 8px 0; font-size: 16px;
}
.modblo-ps__input:focus { outline: none; }
.modblo-ps__close {
background: color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 6%, transparent);
color: inherit; border: 0; cursor: pointer;
font-size: 11px; font-weight: 600;
padding: 5px 8px; border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, monospace;
}
.modblo-ps__body { flex: 1; overflow-y: auto; padding: 16px; }
.modblo-ps__idle-section { margin-bottom: 18px; }
.modblo-ps__idle-section:last-child { margin-bottom: 0; }
.modblo-ps__idle-label {
text-transform: uppercase; letter-spacing: .12em;
font-size: 10px; font-weight: 700; opacity: .55;
margin: 0 0 10px;
}
.modblo-ps__chips { list-style: none; padding: 0; margin: 0; display: flex; flex-wrap: wrap; gap: 6px; }
.modblo-ps__chip {
display: inline-flex; align-items: center;
background: color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 5%, transparent);
color: inherit; text-decoration: none;
padding: 6px 12px; border-radius: 999px;
font-size: 12px; font-weight: 500;
transition: background .15s;
}
.modblo-ps__chip:hover { background: color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 10%, transparent); }
.modblo-ps__loading {
display: flex; align-items: center; gap: 10px; padding: 24px;
font-size: 13px; opacity: .6;
}
.modblo-ps__spinner {
width: 14px; height: 14px; border-radius: 50%;
border: 2px solid color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 12%, transparent);
border-top-color: var(--modblo-ps-accent, #6366f1);
animation: modblo-ps-spin .7s linear infinite;
}
@keyframes modblo-ps-spin { to { transform: rotate(360deg); } }
.modblo-ps__group { margin-bottom: 18px; }
.modblo-ps__group:last-child { margin-bottom: 0; }
.modblo-ps__group-label {
text-transform: uppercase; letter-spacing: .12em;
font-size: 10px; font-weight: 700; opacity: .55;
margin: 0 0 10px;
}
.modblo-ps__hit {
display: flex; align-items: center; gap: 12px;
color: inherit; text-decoration: none;
padding: 8px 10px; border-radius: 10px;
transition: background .15s;
}
.modblo-ps__hit:hover, .modblo-ps__hit.is-focused {
background: color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 5%, transparent);
}
.modblo-ps__hit-img {
width: 44px; height: 44px; border-radius: 8px; object-fit: cover; flex-shrink: 0;
background: color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 5%, transparent);
}
.modblo-ps__hit-body { flex: 1; min-width: 0; }
.modblo-ps__hit-title { font-size: 14px; font-weight: 500; margin: 0 0 2px; line-height: 1.3; }
.modblo-ps__hit-title mark { background: color-mix(in oklab, var(--modblo-ps-accent, #6366f1) 18%, transparent); color: inherit; padding: 0 2px; border-radius: 2px; }
.modblo-ps__hit-meta { font-size: 12px; opacity: .55; margin: 0; }
.modblo-ps__hit-price { font-size: 13px; font-weight: 600; flex-shrink: 0; }
.modblo-ps__empty { padding: 32px 16px; text-align: center; }
.modblo-ps__empty-h { font-size: 15px; font-weight: 600; margin: 0 0 6px; }
.modblo-ps__empty-sub { font-size: 13px; opacity: .65; margin: 0; }
.modblo-ps__empty-sub a { color: var(--modblo-ps-accent, #6366f1); }
.modblo-ps__footer {
border-top: 1px solid color-mix(in oklab, var(--modblo-ps-fg, #0b0b0c) 8%, transparent);
padding: 10px 16px;
}
.modblo-ps__view-all {
display: inline-flex; align-items: center; gap: 6px;
color: var(--modblo-ps-accent, #6366f1); text-decoration: none;
font-size: 13px; font-weight: 600;
}
.modblo-ps__view-all:hover { opacity: .85; }
@media (max-width: 540px) {
.modblo-ps__overlay { padding: 0; }
.modblo-ps__panel { max-height: 100vh; height: 100vh; border-radius: 0; }
.modblo-ps__trigger-label { display: none; }
}
</style>
<script>
(function () {
var root = document.querySelector('[data-modblo-ps][data-section-id="{{ section.id }}"]');
if (!root) return;
var STORAGE_KEY = 'cbPsRecent';
var MAX_RECENT = 5;
var DEBOUNCE_MS = 180;
var allowedTypes = (root.dataset.types || 'product').split(',').map(function (t) { return t.trim(); });
var trigger = root.querySelector('[data-modblo-ps-open]');
var overlay = root.querySelector('[data-modblo-ps-overlay]');
var closeBtn = root.querySelector('[data-modblo-ps-close]');
var input = root.querySelector('[data-modblo-ps-input]');
var idle = root.querySelector('[data-modblo-ps-idle]');
var loading = root.querySelector('[data-modblo-ps-loading]');
var resultsEl = root.querySelector('[data-modblo-ps-results]');
var emptyEl = root.querySelector('[data-modblo-ps-empty]');
var emptyQ = root.querySelector('[data-modblo-ps-empty-q]');
var viewAll = root.querySelector('[data-modblo-ps-view-all]');
var recentSection = root.querySelector('[data-modblo-ps-recent]');
var recentList = root.querySelector('[data-modblo-ps-recent-list]');
function readRecent() {
try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); } catch (e) { return []; }
}
function pushRecent(q) {
if (!q) return;
var list = readRecent().filter(function (t) { return t !== q; });
list.unshift(q);
if (list.length > MAX_RECENT) list = list.slice(0, MAX_RECENT);
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}
function renderRecent() {
var list = readRecent();
if (list.length === 0) { recentSection.hidden = true; return; }
recentSection.hidden = false;
recentList.innerHTML = list.map(function (q) {
var safe = q.replace(/</g, '<').replace(/>/g, '>');
return '<li><a href="/search?q=' + encodeURIComponent(q) + '" class="modblo-ps__chip">' + safe + '</a></li>';
}).join('');
}
function open() {
root.classList.add('is-open');
document.body.style.overflow = 'hidden';
renderRecent();
setTimeout(function () { input.focus(); }, 50);
}
function close() {
root.classList.remove('is-open');
document.body.style.overflow = '';
}
trigger.addEventListener('click', open);
closeBtn.addEventListener('click', close);
overlay.addEventListener('click', function (e) {
if (e.target === overlay) close();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && root.classList.contains('is-open')) close();
// Cmd/Ctrl + K to open
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
if (root.classList.contains('is-open')) close(); else open();
}
});
// Search
var debounceTimer;
var currentQuery = '';
function showState(state) {
idle.hidden = state !== 'idle';
loading.hidden = state !== 'loading';
resultsEl.hidden = state !== 'results';
emptyEl.hidden = state !== 'empty';
}
function fmtMoney(cents) {
return '$' + (cents / 100).toFixed(2);
}
function highlight(text, q) {
if (!q || !text) return text;
var safe = text.replace(/</g, '<').replace(/>/g, '>');
var pattern = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return safe.replace(pattern, '<mark>$1</mark>');
}
function renderResults(data, q) {
var html = '';
var hasResults = false;
if (allowedTypes.indexOf('product') >= 0 && data.resources && data.resources.results && data.resources.results.products && data.resources.results.products.length > 0) {
hasResults = true;
html += '<div class="modblo-ps__group" data-modblo-ps-group>';
html += '<p class="modblo-ps__group-label">Products</p>';
data.resources.results.products.forEach(function (p) {
var img = p.featured_image && p.featured_image.url ? p.featured_image.url.replace(/\.(jpg|jpeg|png|webp)/, '_64x64.$1') : '';
var price = p.price || (p.variants && p.variants[0] && p.variants[0].price) || 0;
html += '<a class="modblo-ps__hit" href="' + p.url + '" data-modblo-ps-hit>' +
(img ? '<img class="modblo-ps__hit-img" src="' + img + '" alt="" loading="lazy">' : '<span class="modblo-ps__hit-img"></span>') +
'<div class="modblo-ps__hit-body">' +
'<p class="modblo-ps__hit-title">' + highlight(p.title, q) + '</p>' +
(p.vendor ? '<p class="modblo-ps__hit-meta">' + p.vendor + '</p>' : '') +
'</div>' +
'<span class="modblo-ps__hit-price">' + fmtMoney(parseFloat(price) * 100) + '</span>' +
'</a>';
});
html += '</div>';
}
if (allowedTypes.indexOf('collection') >= 0 && data.resources && data.resources.results && data.resources.results.collections && data.resources.results.collections.length > 0) {
hasResults = true;
html += '<div class="modblo-ps__group" data-modblo-ps-group>';
html += '<p class="modblo-ps__group-label">Collections</p>';
data.resources.results.collections.forEach(function (c) {
html += '<a class="modblo-ps__hit" href="' + c.url + '" data-modblo-ps-hit>' +
'<span class="modblo-ps__hit-img"></span>' +
'<div class="modblo-ps__hit-body">' +
'<p class="modblo-ps__hit-title">' + highlight(c.title, q) + '</p>' +
'</div>' +
'</a>';
});
html += '</div>';
}
if (allowedTypes.indexOf('page') >= 0 && data.resources && data.resources.results && data.resources.results.pages && data.resources.results.pages.length > 0) {
hasResults = true;
html += '<div class="modblo-ps__group" data-modblo-ps-group>';
html += '<p class="modblo-ps__group-label">Pages</p>';
data.resources.results.pages.forEach(function (pg) {
html += '<a class="modblo-ps__hit" href="' + pg.url + '" data-modblo-ps-hit>' +
'<span class="modblo-ps__hit-img"></span>' +
'<div class="modblo-ps__hit-body">' +
'<p class="modblo-ps__hit-title">' + highlight(pg.title, q) + '</p>' +
'</div>' +
'</a>';
});
html += '</div>';
}
if (hasResults) {
resultsEl.innerHTML = html;
showState('results');
} else {
emptyQ.textContent = q;
showState('empty');
}
}
input.addEventListener('input', function () {
var q = input.value.trim();
currentQuery = q;
viewAll.href = '/search?q=' + encodeURIComponent(q);
clearTimeout(debounceTimer);
if (q.length === 0) { showState('idle'); return; }
if (q.length < 2) return;
showState('loading');
debounceTimer = setTimeout(function () {
var url = '/search/suggest.json?q=' + encodeURIComponent(q) +
'&resources[type]=' + allowedTypes.join(',') +
'&resources[limit]=6&resources[options][unavailable_products]=last';
fetch(url, { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.json(); })
.then(function (data) {
if (input.value.trim() === q) renderResults(data, q);
})
.catch(function () { showState('empty'); });
}, DEBOUNCE_MS);
});
// Persist on Enter (full search)
input.addEventListener('keydown', function (e) {
if (e.key === 'Enter' && currentQuery) {
pushRecent(currentQuery);
window.location.href = '/search?q=' + encodeURIComponent(currentQuery);
}
// Arrow nav through hits
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
var hits = Array.from(root.querySelectorAll('[data-modblo-ps-hit]'));
if (hits.length === 0) return;
var current = hits.findIndex(function (h) { return h.classList.contains('is-focused'); });
var next = e.key === 'ArrowDown' ? (current + 1) % hits.length : (current - 1 + hits.length) % hits.length;
hits.forEach(function (h, i) { h.classList.toggle('is-focused', i === next); });
hits[next].scrollIntoView({ block: 'nearest' });
}
});
// Click on a hit, persist recent
root.addEventListener('click', function (e) {
var hit = e.target.closest('[data-modblo-ps-hit]');
if (hit && currentQuery) pushRecent(currentQuery);
});
})();
</script>
{% schema %}
{
"name": "Predictive Search",
"tag": "section",
"settings": [
{ "type": "header", "content": "Display" },
{ "type": "checkbox", "id": "show_label", "label": "Show 'Search' label on trigger", "default": true },
{ "type": "text", "id": "trigger_label", "label": "Trigger label", "default": "Search" },
{ "type": "text", "id": "input_placeholder", "label": "Input placeholder",
"default": "Search products, collections, articles" },
{ "type": "text", "id": "result_types", "label": "Result types (comma-separated)",
"default": "product,collection,page",
"info": "Any of: product, collection, page, article, query" },
{ "type": "header", "content": "Idle suggestions" },
{ "type": "text", "id": "popular_terms", "label": "Popular search terms (comma-separated)",
"default": "best sellers, new arrivals, gifts under $50" },
{ "type": "header", "content": "Colors" },
{ "type": "color", "id": "bg", "label": "Background", "default": "#ffffff" },
{ "type": "color", "id": "fg", "label": "Foreground", "default": "#0b0b0c" },
{ "type": "color", "id": "accent", "label": "Accent", "default": "#6366f1" }
],
"presets": [{ "name": "Predictive Search" }]
}
{% endschema %}Unlock the section code
Predictive Search is a premium section. Get the full Liquid + scoped CSS paste-ready.
One-time purchase · Lifetime updates · You own the code
Theme editor settings
| Setting | Type | Default |
|---|---|---|
Show 'Search' label on trigger show_label | checkbox | true |
Trigger label trigger_label | text | Search |
Input placeholder input_placeholder | text | , |
Result types result_types Comma-separated. Any of: product, collection, page, article, query. | text | product,collection,page |
Popular search terms popular_terms Comma-separated. Shown as quick chips when the search is empty. | text | , |
Background bg | color | #ffffff |
Foreground fg | color | #0b0b0c |
Accent accent | color | #6366f1 |
SEO & accessibility notes
- Built on Shopify's native /search/suggest.json — no third-party search index, no API key, no GDPR overhead.
- Debounced fetch (180ms) keeps the request count low even for fast typers.
- Real <a> elements per hit, fully keyboard accessible (arrow keys + Enter), full Cmd+K to open.
- Recent searches stored in localStorage only, no cookies.
- Mobile gets a full-screen overlay layout for thumb-friendly scrolling.
Related
