Horizon features a powerful predictive search system with real-time results, multiple resource types, and recently viewed products.
Key Features
- Predictive Search: Real-time search results as you type
- Multiple Resource Types: Products, collections, pages, articles, and query suggestions
- Recently Viewed: Shows recently viewed products when search is empty
- Modal & Inline: Supports both modal overlay and inline search
- Keyboard Navigation: Full keyboard support with arrow keys
- Responsive Design: Optimized for mobile and desktop
Search Modes
Modal Search
Full-screen search overlay activated from header:
{% render 'search', style: 'modal', display_style: 'icon' %}
Features:
- Opens in dialog modal
- Backdrop overlay
- Close button
- Full-width on mobile
- Constrained width on desktop
Inline Search
Embedded search (disabled by default):
{% render 'search', style: 'inline' %}
Search Button
The search action in the header:
<search-button class="search-action">
<button on:click="#search-modal/showDialog"
class="button-unstyled header-actions__action"
aria-label="{{ 'content.search_input_label' | t }}"
aria-haspopup="dialog">
<span class="{% if display_style == 'icon' %}hidden{% else %}mobile:hidden{% endif %}">
{{ 'content.search' | t }}
</span>
<span class="svg-wrapper {% if display_style != 'icon' %}desktop:hidden{% endif %}">
{{ 'icon-search.svg' | inline_asset_content }}
</span>
</button>
</search-button>
Display Styles
Shows magnifying glass icon only.Best for: Minimal headers, mobile Shows “Search” text on desktop, icon on mobile.Best for: Clear CTAs, accessibility
Predictive Search Section
The main search results container:
<div id="predictive-search-results"
class="predictive-search-dropdown"
role="listbox"
aria-label="{{ 'content.search_results_label' | t }}"
aria-expanded="true">
<div class="predictive-search-results__inner" data-search-results>
<!-- Search results -->
</div>
</div>
Result Types
Query Suggestions
Search term suggestions from Shopify:
{% if predictive_search.resources.queries.size > 0 %}
<ul class="predictive-search-results__list predictive-search-results__wrapper-queries">
{% for resource in predictive_search.resources.queries %}
<li class="predictive-search-results__card--query"
ref="resultsItems[]"
data-search-result-index="search-results-{{ forloop.index }}">
<a class="pills__pill predictive-search-results__pill"
href="{{ resource.url }}">
<span aria-label="{{ resource.text }}">
{{ resource.styled_text }}
</span>
</a>
</li>
{% endfor %}
</ul>
{% endif %}
Features:
- Styled matching text
- Pill-style design
- Click to search for term
- Hover animations
Product Results
Product cards in search results:
{% if predictive_search.resources.products.size > 0 %}
{% liquid
assign title = 'content.search_results_resource_products' | t
assign products = predictive_search.resources.products
render 'predictive-search-products-list',
title: title,
products: products
%}
{% endif %}
Displays:
- Product image
- Product title
- Price
- Variant options (if applicable)
- Add to cart button (optional)
Collection Results
Collection cards with images:
{% if predictive_search.resources.collections.size > 0 %}
{% assign resource_title = 'content.search_results_resource_collections' | t %}
{% render 'predictive-search-resource-carousel',
title: resource_title,
resource_type: 'collection',
resources: predictive_search.resources.collections
%}
{% endif %}
Page Results
Content pages matching search:
{% if predictive_search.resources.pages.size > 0 %}
{% render 'predictive-search-resource-carousel',
title: 'Pages',
resource_type: 'page',
resources: predictive_search.resources.pages
%}
{% endif %}
Article Results
Blog posts matching search:
{% if predictive_search.resources.articles.size > 0 %}
{% render 'predictive-search-resource-carousel',
title: 'Articles',
resource_type: 'article',
resources: predictive_search.resources.articles
%}
{% endif %}
Recently Viewed Products
When search input is empty, show recently viewed:
{% else %}
{% liquid
assign title = 'content.recently_viewed_products' | t
assign products = search.results
comment
Searching for recently viewed products by id doesn't preserve order.
Get product ids into array to reorder them.
endcomment
if search.terms contains 'id:'
assign new_products_ids = search.terms | replace: 'id:', '' | split: ' OR '
endif
render 'predictive-search-products-list',
title: title,
products: products,
order_ids: new_products_ids,
limit: 4
%}
{% endif %}
How It Works:
- Stores viewed product IDs in localStorage
- Searches for those products by ID
- Reorders results to match view order
- Limits to 4 most recent
Search Results Counter
{% if predictive_search.performed %}
{% assign search_results_count =
predictive_search.resources.products.size
| plus: predictive_search.resources.pages.size
| plus: predictive_search.resources.articles.size
| plus: predictive_search.resources.collections.size
| plus: predictive_search.resources.queries.size
%}
{% endif %}
Accessibility Announcements
<div class="visually-hidden" role="status" aria-live="polite">
{% if predictive_search.performed and search_results_count > 0 %}
{{ 'accessibility.search_results_count' | t:
count: search_results_count,
query: predictive_search.terms
}}
{% elsif predictive_search.performed and search_results_count == 0 %}
{{ 'accessibility.search_results_no_results' | t:
query: predictive_search.terms
}}
{% endif %}
</div>
No Results State
{% if search_results_count == 0 %}
<p class="predictive-search-results__no-results">
{{ 'content.search_results_no_results' | t: terms: predictive_search.terms }}
</p>
{% endif %}
Single Result Handling
When only one result, provide quick navigation:
{% assign total_results =
predictive_search.resources.products.size
| plus: predictive_search.resources.collections.size
| plus: predictive_search.resources.pages.size
| plus: predictive_search.resources.articles.size
%}
{% if total_results == 1 %}
{% if predictive_search.resources.products.size == 1 %}
{% assign single_result_url = predictive_search.resources.products.first.url %}
{% elsif predictive_search.resources.collections.size == 1 %}
{% assign single_result_url = predictive_search.resources.collections.first.url %}
{% endif %}
<div data-single-result-url="{{ single_result_url }}"></div>
{% endif %}
Behavior: Pressing Enter navigates directly to the single result.
Animations
Slide-Up Animation
@keyframes search-element-slide-up {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.predictive-search-results__wrapper {
animation: search-element-slide-up
var(--animation-speed-medium)
var(--animation-timing-bounce) backwards;
}
Staggered Delays
.predictive-search-results__wrapper-queries {
animation-delay: 50ms;
}
.predictive-search-results__list:nth-of-type(2) {
animation-delay: 150ms;
}
.predictive-search-results__list:nth-of-type(3) {
animation-delay: 200ms;
}
.predictive-search-results__list:nth-of-type(4) {
animation-delay: 250ms;
}
Removing Animation
.predictive-search-results__wrapper.removing {
animation: search-element-slide-down
var(--animation-speed-medium)
var(--animation-timing-fade-out) forwards;
}
@keyframes search-element-slide-down {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(8px);
}
}
Card Hover Effects
Product Cards
.predictive-search-results__card--product {
transition: transform var(--animation-speed-medium),
background-color var(--animation-speed-medium),
border-color var(--animation-speed-medium);
}
.predictive-search-results__card--product:hover {
background-color: var(--card-bg-hover);
border-radius: var(--product-corner-radius);
padding: calc(var(--padding-2xs) + 2px);
margin: calc((var(--padding-2xs) + 2px) * -1);
}
.predictive-search-results__card--product:active {
transform: scale(0.97);
transition: transform 100ms var(--animation-timing-active);
}
Query Pills
.predictive-search-results__pill {
font-weight: 500;
white-space: nowrap;
color: var(--color-foreground);
transition: background-color var(--animation-speed-medium),
box-shadow var(--animation-speed-medium),
transform var(--animation-speed-medium);
margin: 2px;
}
.predictive-search-results__pill:hover {
transform: scale(1.03);
box-shadow: 0 2px 5px rgb(0 0 0 / var(--opacity-8));
}
Keyboard Navigation
Arrow Key Support
<li ref="resultsItems[]"
data-search-result-index="search-results-{{ forloop.index }}"
on:keydown="/onSearchKeyDown">
<!-- Result content -->
</li>
Supported Keys:
↑ / ↓ - Navigate results
Enter - Select result
Esc - Close search
Tab - Navigate normally
Focus Styles
.predictive-search-results__card:is([aria-selected='true'].keyboard-focus,
:focus-visible) {
background-color: var(--card-bg-hover);
outline: var(--border-width-sm) solid var(--color-border);
border-color: var(--card-border-focus);
}
Carousel Layout
For collections, pages, and articles:
.predictive-search-results__list {
--slide-width: 27.5%;
--slideshow-gap: var(--gap-md);
}
.predictive-search-results__card {
flex: 0 0 auto;
scroll-snap-align: start;
width: 60cqi; /* Container query units */
}
@media screen and (min-width: 750px) {
.predictive-search-results__card {
width: 27.5cqi;
}
}
Features:
- Horizontal scroll
- Scroll snap
- Arrow navigation on desktop
- Touch-friendly on mobile
Modal Styling
Desktop Modal
.dialog-modal .predictive-search-form__header {
border-bottom: var(--search-border-width) solid var(--color-border);
background-color: var(--color-background);
@media screen and (min-width: 750px) {
padding: var(--padding-2xs) var(--padding-2xs) 0;
}
}
.search-modal__content .predictive-search-form__content {
max-height: var(--modal-max-height);
}
Mobile Modal
@media screen and (max-width: 749px) {
.dialog-modal[open] {
border-radius: 0;
}
.dialog-modal .predictive-search__close-modal-button {
padding-inline-start: var(--margin-xs);
}
}
Input Styling
input[type='search']::-webkit-search-decoration {
-webkit-appearance: none;
}
.predictive-search:has(.predictive-search-dropdown) .search-input {
outline-color: transparent;
}
.predictive-search:has(.predictive-search-dropdown[aria-expanded='true'])
.predictive-search-form__header-inner:focus-within {
border-radius: var(--search-border-radius);
}
Schema Configuration
{
"name": "Predictive Search",
"settings": [],
"blocks": [
{
"type": "@theme"
}
]
}
The predictive search section has no configurable settings—behavior is controlled by the search button in the header.
Search Performance
Debouncing
Search queries are debounced to reduce API calls:
let searchTimeout;
function performSearch(query) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
fetch(`/search/suggest.json?q=${query}&resources[type]=product,collection,page,article`);
}, 300);
}
Caching
Recent searches are cached in memory:
const searchCache = new Map();
if (searchCache.has(query)) {
return searchCache.get(query);
}
Accessibility
<div role="status" aria-live="polite" aria-atomic="true">
{{ search_results_count }} results for "{{ predictive_search.terms }}"
</div>
<div role="listbox" aria-label="Search results">
<div role="option" aria-selected="false">Result 1</div>
<div role="option" aria-selected="true">Result 2</div>
</div>
- Focus moves to search input when modal opens
- Focus returns to trigger button when modal closes
- Focus visible on all interactive elements
- Keyboard shortcuts work globally