Search and Predictive Search Intermediate 12 min read

Rendering Predictive Results

Build the section template for predictive search results, including products, collections, articles, and query suggestions with proper styling.

The predictive search section renders results returned by the search suggest API. Let’s build a comprehensive results template that handles all resource types.

The Predictive Search Object

When using Section Rendering with /search/suggest, you get access to predictive_search:

{{ predictive_search.performed }} {# true if search was done #}
{{ predictive_search.terms }} {# The search query #}
{{ predictive_search.resources }} {# Object with results by type #}
{# Access specific resource types #}
{{ predictive_search.resources.products }}
{{ predictive_search.resources.collections }}
{{ predictive_search.resources.articles }}
{{ predictive_search.resources.pages }}
{{ predictive_search.resources.queries }} {# Suggested searches #}

Basic Results Section

{# sections/predictive-search.liquid #}
{%- if predictive_search.performed -%}
<div class="predictive-search__results" id="predictive-search-results">
{%- liquid
assign has_results = false
if predictive_search.resources.products.size > 0
assign has_results = true
elsif predictive_search.resources.collections.size > 0
assign has_results = true
elsif predictive_search.resources.articles.size > 0
assign has_results = true
elsif predictive_search.resources.pages.size > 0
assign has_results = true
endif
-%}
{%- if has_results -%}
{# Products #}
{%- if predictive_search.resources.products.size > 0 -%}
<div class="predictive-search__group">
<h3 class="predictive-search__group-heading">Products</h3>
<ul class="predictive-search__products">
{%- for product in predictive_search.resources.products -%}
<li>
{% render 'predictive-search-product', product: product %}
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{# Collections #}
{%- if predictive_search.resources.collections.size > 0 -%}
<div class="predictive-search__group">
<h3 class="predictive-search__group-heading">Collections</h3>
<ul class="predictive-search__collections">
{%- for collection in predictive_search.resources.collections -%}
<li>
{% render 'predictive-search-collection', collection: collection %}
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{# Articles #}
{%- if predictive_search.resources.articles.size > 0 -%}
<div class="predictive-search__group">
<h3 class="predictive-search__group-heading">Articles</h3>
<ul class="predictive-search__articles">
{%- for article in predictive_search.resources.articles -%}
<li>
{% render 'predictive-search-article', article: article %}
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}
{# View all link #}
<div class="predictive-search__footer">
<a href="{{ routes.search_url }}?q={{ predictive_search.terms | url_encode }}" class="predictive-search__view-all">
View all results for "{{ predictive_search.terms }}"
</a>
</div>
{%- else -%}
<div class="predictive-search__no-results">
<p>No results found for "{{ predictive_search.terms }}"</p>
</div>
{%- endif -%}
</div>
{%- endif -%}

Product Result Snippet

{# snippets/predictive-search-product.liquid #}
<a href="{{ product.url }}" class="predictive-product" role="option">
<div class="predictive-product__image">
{%- if product.featured_image -%}
<img
src="{{ product.featured_image | image_url: width: 100 }}"
alt="{{ product.featured_image.alt | default: product.title }}"
width="50"
height="50"
loading="lazy"
>
{%- else -%}
<div class="predictive-product__placeholder">
{% render 'icon-image' %}
</div>
{%- endif -%}
</div>
<div class="predictive-product__content">
<p class="predictive-product__title">
{{ product.title }}
</p>
<div class="predictive-product__price">
{%- if product.compare_at_price > product.price -%}
<span class="predictive-product__price--sale">
{{ product.price | money }}
</span>
<span class="predictive-product__price--compare">
{{ product.compare_at_price | money }}
</span>
{%- else -%}
<span>{{ product.price | money }}</span>
{%- endif -%}
</div>
</div>
</a>

Collection Result Snippet

{# snippets/predictive-search-collection.liquid #}
<a href="{{ collection.url }}" class="predictive-collection" role="option">
{%- if collection.image -%}
<div class="predictive-collection__image">
<img
src="{{ collection.image | image_url: width: 100 }}"
alt="{{ collection.image.alt | default: collection.title }}"
width="50"
height="50"
loading="lazy"
>
</div>
{%- endif -%}
<div class="predictive-collection__content">
<p class="predictive-collection__title">{{ collection.title }}</p>
<p class="predictive-collection__count">
{{ collection.products_count }} {{ collection.products_count | pluralize: 'product', 'products' }}
</p>
</div>
</a>

Article Result Snippet

{# snippets/predictive-search-article.liquid #}
<a href="{{ article.url }}" class="predictive-article" role="option">
{%- if article.image -%}
<div class="predictive-article__image">
<img
src="{{ article.image | image_url: width: 100 }}"
alt="{{ article.image.alt }}"
width="50"
height="50"
loading="lazy"
>
</div>
{%- endif -%}
<div class="predictive-article__content">
<p class="predictive-article__title">{{ article.title }}</p>
<p class="predictive-article__meta">
{{ article.published_at | date: '%b %d, %Y' }}
{%- if article.author -%}
· {{ article.author }}
{%- endif -%}
</p>
</div>
</a>

Query Suggestions

Show suggested search terms:

{%- if predictive_search.resources.queries.size > 0 -%}
<div class="predictive-search__group">
<h3 class="predictive-search__group-heading">Suggestions</h3>
<ul class="predictive-search__queries">
{%- for query in predictive_search.resources.queries -%}
<li>
<a
href="{{ routes.search_url }}?q={{ query.text | url_encode }}"
class="predictive-query"
role="option"
>
{% render 'icon-search' %}
<span>{{ query.styled_text }}</span>
</a>
</li>
{%- endfor -%}
</ul>
</div>
{%- endif -%}

The styled_text property includes <mark> tags around matching portions.

Complete Predictive Search Styles

/* Results container */
.predictive-search__results {
position: absolute;
top: 100%;
left: 0;
right: 0;
max-height: 80vh;
overflow-y: auto;
background: var(--color-background);
border: 1px solid var(--color-border);
border-top: none;
border-radius: 0 0 var(--border-radius) var(--border-radius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
z-index: 100;
}
/* Groups */
.predictive-search__group {
padding: var(--spacing-md);
border-bottom: 1px solid var(--color-border);
}
.predictive-search__group:last-child {
border-bottom: none;
}
.predictive-search__group-heading {
font-size: 0.6875rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--color-text-light);
margin: 0 0 var(--spacing-sm);
}
/* Lists */
.predictive-search__products,
.predictive-search__collections,
.predictive-search__articles,
.predictive-search__queries {
list-style: none;
padding: 0;
margin: 0;
}
/* Product item */
.predictive-product {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
text-decoration: none;
color: inherit;
border-radius: var(--border-radius);
transition: background-color 0.15s;
}
.predictive-product:hover,
.predictive-product:focus,
.predictive-product[aria-selected="true"] {
background: var(--color-background-secondary);
outline: none;
}
.predictive-product__image {
width: 50px;
height: 50px;
flex-shrink: 0;
}
.predictive-product__image img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--border-radius);
}
.predictive-product__placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: var(--color-background-secondary);
border-radius: var(--border-radius);
color: var(--color-text-light);
}
.predictive-product__content {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
justify-content: center;
}
.predictive-product__title {
font-size: 0.875rem;
font-weight: 500;
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.predictive-product__price {
font-size: 0.8125rem;
margin: 2px 0 0;
}
.predictive-product__price--sale {
color: var(--color-sale);
}
.predictive-product__price--compare {
text-decoration: line-through;
color: var(--color-text-light);
margin-left: var(--spacing-xs);
}
/* Collection item */
.predictive-collection {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
text-decoration: none;
color: inherit;
border-radius: var(--border-radius);
}
.predictive-collection:hover,
.predictive-collection:focus {
background: var(--color-background-secondary);
}
.predictive-collection__image {
width: 50px;
height: 50px;
flex-shrink: 0;
}
.predictive-collection__image img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: var(--border-radius);
}
.predictive-collection__title {
font-size: 0.875rem;
font-weight: 500;
margin: 0;
}
.predictive-collection__count {
font-size: 0.75rem;
color: var(--color-text-light);
margin: 2px 0 0;
}
/* Article item */
.predictive-article {
display: flex;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
text-decoration: none;
color: inherit;
border-radius: var(--border-radius);
}
.predictive-article:hover,
.predictive-article:focus {
background: var(--color-background-secondary);
}
.predictive-article__title {
font-size: 0.875rem;
font-weight: 500;
margin: 0;
}
.predictive-article__meta {
font-size: 0.75rem;
color: var(--color-text-light);
margin: 2px 0 0;
}
/* Query suggestions */
.predictive-query {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
text-decoration: none;
color: inherit;
border-radius: var(--border-radius);
}
.predictive-query:hover,
.predictive-query:focus {
background: var(--color-background-secondary);
}
.predictive-query svg {
width: 16px;
height: 16px;
color: var(--color-text-light);
}
.predictive-query mark {
background: none;
font-weight: 600;
}
/* Footer */
.predictive-search__footer {
padding: var(--spacing-md);
text-align: center;
border-top: 1px solid var(--color-border);
}
.predictive-search__view-all {
font-size: 0.875rem;
color: var(--color-primary);
}
/* No results */
.predictive-search__no-results {
padding: var(--spacing-xl);
text-align: center;
color: var(--color-text-light);
}

Keyboard Navigation

Add arrow key support for results:

class PredictiveSearch extends HTMLElement {
onKeydown(e) {
const items = this.querySelectorAll('[role="option"]');
const currentIndex = Array.from(items).findIndex(
item => item.getAttribute('aria-selected') === 'true'
);
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
this.selectItem(items, currentIndex + 1);
break;
case 'ArrowUp':
e.preventDefault();
this.selectItem(items, currentIndex - 1);
break;
case 'Enter':
if (currentIndex >= 0) {
e.preventDefault();
items[currentIndex].click();
}
break;
case 'Escape':
this.close();
this.input.focus();
break;
}
}
selectItem(items, index) {
// Deselect all
items.forEach(item => item.setAttribute('aria-selected', 'false'));
// Wrap around
if (index < 0) index = items.length - 1;
if (index >= items.length) index = 0;
// Select new item
items[index].setAttribute('aria-selected', 'true');
items[index].scrollIntoView({ block: 'nearest' });
}
}

Highlighting Matches

Highlight the search term in results:

{%- assign title_parts = product.title | split: predictive_search.terms -%}
<p class="predictive-product__title">
{%- for part in title_parts -%}
{{ part }}
{%- unless forloop.last -%}
<mark>{{ predictive_search.terms }}</mark>
{%- endunless -%}
{%- endfor -%}
</p>

Or use CSS for a simpler approach with ::highlight() (limited browser support).

Schema for Configuration

{% schema %}
{
"name": "Predictive Search",
"settings": [
{
"type": "range",
"id": "product_limit",
"label": "Product results limit",
"min": 1,
"max": 10,
"default": 4
},
{
"type": "range",
"id": "collection_limit",
"label": "Collection results limit",
"min": 0,
"max": 5,
"default": 2
},
{
"type": "range",
"id": "article_limit",
"label": "Article results limit",
"min": 0,
"max": 5,
"default": 2
},
{
"type": "checkbox",
"id": "show_vendor",
"label": "Show product vendor",
"default": false
}
]
}
{% endschema %}

Practice Exercise

Build a predictive search results section that:

  1. Shows products with images and prices
  2. Shows collections with product counts
  3. Shows articles with dates
  4. Includes a “View all results” link
  5. Supports keyboard navigation

Test with:

  • Various search queries
  • Queries with no results
  • Fast typing (debounce)
  • Keyboard-only navigation

Key Takeaways

  1. predictive_search.resources contains results by type
  2. Create snippets for each result type
  3. Query suggestions use styled_text for highlighting
  4. Add role="option" for accessibility
  5. Keyboard navigation enhances usability
  6. Style hover and selected states clearly
  7. Include “View all” link to full results
  8. Handle no results gracefully

What’s Next?

The next lesson covers No Results UX and Query Handling for improving search when nothing is found.

Finished this lesson?

Mark it complete to track your progress.

Discussion

Loading comments...