Search Template Fundamentals
Understand the search template structure, the search object, and how to display search results for products, pages, and articles.
Search helps customers find exactly what they’re looking for. Understanding Shopify’s search template and the search object is essential for building effective search experiences.
The Search Object
When a search is performed, the search object contains the query and results:
{{ search.terms }} {# The search query: "blue shirt" #}{{ search.results }} {# Array of matching items #}{{ search.results_count }} {# Total number of results #}{{ search.performed }} {# true if a search was performed #}{{ search.types }} {# Resource types searched #}Basic Search Template
{# templates/search.liquid #}
<div class="search-page"> <h1 class="search-page__title">Search</h1>
{# Search form #} <form action="{{ routes.search_url }}" method="get" class="search-form"> <input type="search" name="q" value="{{ search.terms | escape }}" placeholder="Search products..." aria-label="Search" > <button type="submit">Search</button> </form>
{# Results #} {%- if search.performed -%} {%- if search.results_count > 0 -%} <p class="search-page__count"> {{ search.results_count }} results for "{{ search.terms }}" </p>
<div class="search-results"> {%- for item in search.results -%} {% render 'search-result-item', item: item %} {%- endfor -%} </div>
{%- if paginate.pages > 1 -%} {% render 'pagination', paginate: paginate %} {%- endif -%} {%- else -%} {% render 'search-no-results', terms: search.terms %} {%- endif -%} {%- endif -%}</div>Search Form
The search form submits to /search with the query in q:
{# snippets/search-form.liquid #}
<form action="{{ routes.search_url }}" method="get" role="search" class="search-form"> <label for="search-input" class="visually-hidden">Search</label>
<div class="search-form__input-wrapper"> <span class="search-form__icon"> {% render 'icon-search' %} </span>
<input type="search" id="search-input" name="q" value="{{ search.terms | escape }}" placeholder="Search our store..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" class="search-form__input" >
{%- if search.terms != blank -%} <button type="button" class="search-form__clear" aria-label="Clear search" onclick="this.previousElementSibling.value = ''; this.previousElementSibling.focus();" > {% render 'icon-close' %} </button> {%- endif -%} </div>
<button type="submit" class="search-form__submit"> Search </button></form>Filtering by Resource Type
Limit search to specific types:
{# Search only products #}<input type="hidden" name="type" value="product">
{# Search products and articles #}<input type="hidden" name="type" value="product,article">
{# Available types: product, article, page #}Or let users choose:
<select name="type" class="search-form__type"> <option value="product,article,page">All</option> <option value="product">Products</option> <option value="article">Blog Posts</option> <option value="page">Pages</option></select>Search Result Item
Handle different result types:
{# snippets/search-result-item.liquid #}
<div class="search-result" data-type="{{ item.object_type }}"> {%- case item.object_type -%} {%- when 'product' -%} {% render 'search-result-product', product: item %}
{%- when 'article' -%} {% render 'search-result-article', article: item %}
{%- when 'page' -%} {% render 'search-result-page', page: item %}
{%- endcase -%}</div>Product Result
{# snippets/search-result-product.liquid #}
<a href="{{ product.url }}" class="search-result-product"> <div class="search-result-product__image"> {%- if product.featured_image -%} <img src="{{ product.featured_image | image_url: width: 200 }}" alt="{{ product.featured_image.alt | default: product.title }}" width="100" height="100" loading="lazy" > {%- else -%} {{ 'product-1' | placeholder_svg_tag }} {%- endif -%} </div>
<div class="search-result-product__content"> <h3 class="search-result-product__title">{{ product.title }}</h3>
{%- if product.vendor -%} <p class="search-result-product__vendor">{{ product.vendor }}</p> {%- endif -%}
<p class="search-result-product__price"> {{ product.price | money }} {%- if product.compare_at_price > product.price -%} <span class="search-result-product__compare-price"> {{ product.compare_at_price | money }} </span> {%- endif -%} </p> </div></a>Article Result
{# snippets/search-result-article.liquid #}
<a href="{{ article.url }}" class="search-result-article"> {%- if article.image -%} <div class="search-result-article__image"> <img src="{{ article.image | image_url: width: 200 }}" alt="{{ article.image.alt }}" width="100" height="100" loading="lazy" > </div> {%- endif -%}
<div class="search-result-article__content"> <p class="search-result-article__type">Blog Post</p> <h3 class="search-result-article__title">{{ article.title }}</h3> <p class="search-result-article__excerpt"> {{ article.excerpt_or_content | strip_html | truncate: 120 }} </p> <p class="search-result-article__meta"> {{ article.published_at | date: '%B %d, %Y' }} </p> </div></a>Page Result
{# snippets/search-result-page.liquid #}
<a href="{{ page.url }}" class="search-result-page"> <div class="search-result-page__content"> <p class="search-result-page__type">Page</p> <h3 class="search-result-page__title">{{ page.title }}</h3> <p class="search-result-page__excerpt"> {{ page.content | strip_html | truncate: 120 }} </p> </div></a>Paginated Results
Wrap results in paginate tag:
{%- paginate search.results by 24 -%} <div class="search-results"> {%- for item in search.results -%} {% render 'search-result-item', item: item %} {%- endfor -%} </div>
{%- if paginate.pages > 1 -%} {% render 'pagination', paginate: paginate %} {%- endif -%}{%- endpaginate -%}Search Page Styles
.search-page { max-width: 1200px; margin: 0 auto; padding: var(--spacing-xl) var(--spacing-md);}
.search-page__title { text-align: center; margin-bottom: var(--spacing-lg);}
.search-form { display: flex; gap: var(--spacing-sm); max-width: 600px; margin: 0 auto var(--spacing-xl);}
.search-form__input-wrapper { flex: 1; position: relative; display: flex; align-items: center;}
.search-form__icon { position: absolute; left: var(--spacing-md); color: var(--color-text-light); pointer-events: none;}
.search-form__input { width: 100%; padding: var(--spacing-md); padding-left: calc(var(--spacing-md) * 2 + 20px); font-size: 1rem; border: 1px solid var(--color-border); border-radius: var(--border-radius);}
.search-form__clear { position: absolute; right: var(--spacing-sm); padding: var(--spacing-xs); background: none; border: none; cursor: pointer; color: var(--color-text-light);}
.search-form__submit { padding: var(--spacing-md) var(--spacing-lg);}
.search-page__count { color: var(--color-text-light); margin-bottom: var(--spacing-lg);}
/* Results grid */.search-results { display: grid; gap: var(--spacing-md);}
/* Product results */.search-result-product { display: flex; gap: var(--spacing-md); padding: var(--spacing-md); border: 1px solid var(--color-border); border-radius: var(--border-radius); text-decoration: none; color: inherit; transition: border-color 0.2s;}
.search-result-product:hover { border-color: var(--color-text);}
.search-result-product__image { width: 100px; height: 100px; flex-shrink: 0;}
.search-result-product__image img { width: 100%; height: 100%; object-fit: cover; border-radius: var(--border-radius);}
.search-result-product__title { font-size: 1rem; margin: 0 0 var(--spacing-xs);}
.search-result-product__vendor { font-size: 0.8125rem; color: var(--color-text-light); margin: 0 0 var(--spacing-xs);}
.search-result-product__price { font-weight: 600; margin: 0;}
.search-result-product__compare-price { text-decoration: line-through; color: var(--color-text-light); font-weight: 400; margin-left: var(--spacing-xs);}
/* Article/Page results */.search-result-article,.search-result-page { display: flex; gap: var(--spacing-md); padding: var(--spacing-md); border: 1px solid var(--color-border); border-radius: var(--border-radius); text-decoration: none; color: inherit;}
.search-result-article__type,.search-result-page__type { font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--color-text-light); margin: 0 0 var(--spacing-xs);}
.search-result-article__title,.search-result-page__title { font-size: 1rem; margin: 0 0 var(--spacing-xs);}
.search-result-article__excerpt,.search-result-page__excerpt { font-size: 0.875rem; color: var(--color-text-light); margin: 0;}Complete Search Template
{# templates/search.liquid #}
{%- paginate search.results by 24 -%} <div class="search-page"> <h1 class="search-page__title"> {%- if search.performed -%} Search Results {%- else -%} Search {%- endif -%} </h1>
{# Search form #} {% render 'search-form' %}
{# Results #} {%- if search.performed -%} {%- if search.results_count > 0 -%} <p class="search-page__count"> Found {{ search.results_count }} {{ search.results_count | pluralize: 'result', 'results' }} for "{{ search.terms }}" </p>
<div class="search-results"> {%- for item in search.results -%} {% render 'search-result-item', item: item %} {%- endfor -%} </div>
{%- if paginate.pages > 1 -%} {% render 'pagination', paginate: paginate %} {%- endif -%} {%- else -%} {% render 'search-no-results', terms: search.terms %} {%- endif -%} {%- else -%} <div class="search-prompt"> <p>Enter a search term to find products, articles, and pages.</p> </div> {%- endif -%} </div>{%- endpaginate -%}Practice Exercise
Build a search results page that:
- Has a search form with the current query
- Shows result count
- Displays products with images and prices
- Shows articles with excerpts
- Uses pagination for many results
Test with:
- Product searches
- Article/blog searches
- Empty search queries
- Long result sets
Key Takeaways
search.termscontains the querysearch.resultsis an array of mixed typessearch.performedchecks if a search was done- Filter by type with
name="type"input - Check
object_typeto render different result types - Use pagination for large result sets
- Always escape search terms in output
What’s Next?
The next lesson covers Predictive Search Concepts and UI Patterns for live search-as-you-type experiences.
Finished this lesson?
Mark it complete to track your progress.
Discussion
Loading comments...