Search and Predictive Search Beginner 10 min read

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:

  1. Has a search form with the current query
  2. Shows result count
  3. Displays products with images and prices
  4. Shows articles with excerpts
  5. Uses pagination for many results

Test with:

  • Product searches
  • Article/blog searches
  • Empty search queries
  • Long result sets

Key Takeaways

  1. search.terms contains the query
  2. search.results is an array of mixed types
  3. search.performed checks if a search was done
  4. Filter by type with name="type" input
  5. Check object_type to render different result types
  6. Use pagination for large result sets
  7. 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...