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:
- Shows products with images and prices
- Shows collections with product counts
- Shows articles with dates
- Includes a “View all results” link
- Supports keyboard navigation
Test with:
- Various search queries
- Queries with no results
- Fast typing (debounce)
- Keyboard-only navigation
Key Takeaways
predictive_search.resourcescontains results by type- Create snippets for each result type
- Query suggestions use
styled_textfor highlighting - Add
role="option"for accessibility - Keyboard navigation enhances usability
- Style hover and selected states clearly
- Include “View all” link to full results
- 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...