Skip to main content
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

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
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.
display_style: 'icon'
Best for: Minimal headers, mobile

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:
  1. Stores viewed product IDs in localStorage
  2. Searches for those products by ID
  3. Reorders results to match view order
  4. 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);
}
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

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

Build docs developers (and LLMs) love