Predictive Search Moved Off Shopify Onto an External Search API With Client-Side Templating
The type-ahead rebuilt from the inside: same custom element, same keyboard model, same ARIA contract — a different data source and a render step that did not exist before.
Dawn's predictive search fetches HTML from /search/suggest and injects it, so changing the data source to JSON removes the one thing the element knows how to do. We kept Dawn's custom element — its combobox lifecycle, roving keyboard selection, live region and per-keystroke abort — and replaced only the fetch and the render, compiling a Liquid-authored Handlebars template in the browser.
Fact Strip
- Client: Nudestix
- Surfaces: Global and search
- Complexity: High
- Attribution: Built on top of Dawn 6.0.2's predictive-search custom element; this is our modification of it. The element's lifecycle, its combobox semantics and its keyboard model are Dawn's. The data source, the response normalization, the templating layer and the merchandising slots are ours. The search service behind it is not ours. It is client infrastructure operated outside this theme, and this page describes the storefront side only.
- Status: Live, verified 2026-09-06
- Evidence: Two code artifacts plus twenty-two client working records
- Confidence: Strong
- Platform primitives: 3
The Problem
Shopify's predictive search is an index and a renderer bolted together. It returns products, collections, pages, articles and query suggestions from Shopify's own index, and it hands the theme a server-rendered fragment of HTML to drop into the panel. That is a good default and a hard ceiling. A brand cannot change how results are ranked, cannot group them differently, cannot add a related-search row that Shopify does not produce, and cannot put anything of its own inside the panel — because the panel arrives already rendered. Nudestix needed all four: ranking and grouping from a dedicated search service, related searches, and merchandising slots in the results panel.
The Constraint
Dawn's predictive-search element assumes the shape it was built for. It fetches /search/suggest with a section id, receives HTML, and injects it — every step downstream of the response assumes markup. Once results arrive as JSON there is nothing to inject, so the theme has to acquire a templating step it never had. What could not be given up in exchange is the part of the element that has nothing to do with data: the combobox roles, aria-activedescendant management, the live region, arrow-key selection, the abort behavior on fast typing. Those are the parts that are expensive to get right and invisible when they are. And the results markup still had to be authorable in Liquid so merchandisers could edit the panel — while Handlebars and Liquid both claim {{ }}, which makes a shared file illegal in one language or the other.
What We Built
predictive-search.js keeps Dawn's shape exactly: a PredictiveSearch extending SearchForm, the same open, close and closeResults lifecycle, aria-expanded handling, roving ArrowUp and ArrowDown selection over visible options, focus and focusout behavior, per-instance result caching, and one AbortController per query. What changed is a single method. getSearchResults() now sends the query to the external search service with the abort signal attached, instead of to Shopify.
Around that sit two helpers. A response helper normalizes the payload into the shape the panel expects — collections, blog posts and related searches — appends a width parameter to product image URLs so the panel does not download full-resolution art into a dropdown, and derives the per-group counts and totals the layout and the live region both read. A template helper compiles the markup held in a predictive-search-template script tag after reconfiguring Handlebars to [[ ]] delimiters, so one file can be valid Liquid and a valid Handlebars template simultaneously.
That template is predictive-search.liquid, rendered into the page by both header-search.liquid and main-search.liquid — the header modal and the search page host the same template holder, which is why both surfaces render identical panels from one source. A formatProductTitle helper strips a promotional price string out of product titles and renders a trailing parenthetical in a lighter weight, which is a display-side answer to promo copy that was typed into the titles upstream.
What a merchandiser still owns. The Liquid side of the template carries two offer slots, each behind its own enable switch with text, description, link, image and color settings, and an explicit empty state for every result group — "no collections found" rather than a group that silently disappears. Those are section settings on the header, so the panel is edited beside the logo and the announcement bar rather than in a separate admin, and the offer cards are in the initial HTML even when the results next to them are not. adjustLTOImagePadding() measures the rendered panel above 1280px and writes a --LTO-PT custom property, so an offer card scales against however tall the results turned out.
The whole stack is gated behind the theme's predictive-search setting in theme.liquid, which is what loads the Handlebars build, search-form.js and predictive-search.js. Turn the setting off and none of it ships.
Why This Way
Replacing only the data source and the render step is the smallest change that buys an external index. The keyboard model, the live region, the focus handling and the abort logic are the parts of a search panel most likely to be broken by a rewrite, and the parts nobody notices until they break. None of them care where results come from, so none of them changed.
Templating from a Liquid-authored file is what kept the theme editor in the picture. The results are client-rendered, but the markup around them is still theme code — which means offer cards, copy and empty states stay a merchandiser's job. Switching Handlebars to [[ ]] is the whole mechanism that makes that legal.
The bill arrives in two places. This theme now carries a second templating language, and the file that matters most has both of them in it — anyone editing that template has to know which brackets belong to which engine, which is a real onboarding tax. The second is architectural: the storefront's type-ahead depends on a service outside the theme, and owns the shape of the response it expects. That is a decision to make deliberately. It was made here.
Why Not an App
Search apps ship their own panel: their markup, their styles, their idea of what belongs in a dropdown. Any brand that has invested in the type, spacing and merchandising of its header gets that back as a vendor component, and the theme's merchandising slots and its keyboard model are not part of the deal. Keeping the panel as theme code and treating the search service as a data source draws the line somewhere more useful — the service owns ranking and relevance, which is the thing it is genuinely better at, and the theme owns everything a shopper looks at. The cost is that the integration is now ours to maintain: the response contract, the normalization, the templating and the accessibility all live in the theme rather than in a vendor's release notes.
Implementation Notes
- The endpoint is a bespoke search service on the client's own infrastructure rather than Shopify's
/search/suggest.json, and it is called with the query and the abort signal only. - Handlebars is reconfigured to
[[ ]]through a delimiter helper carried in the theme's own Handlebars build, so one file stays legal in both templating languages. - The template is delivered as a
text/x-handlebars-templatescript tag from both the header search snippet and the main search section, so both surfaces compile identical markup. - Results are cached per normalized term across every predictive-search instance on the page, so the header and the search page share one cache rather than fetching the same term twice.
- One AbortController per in-flight query, replaced whenever the form resets, so each keystroke cancels the request before it rather than racing two responses into the same panel.
formatProductTitlestrips a promotional price string out of product titles and renders a trailing parenthetical in a lighter weight — a display-side cleanup for promo text baked into titles upstream.- Every result group renders an explicit empty state instead of collapsing, so a shopper is told there are no collections rather than shown a panel that quietly reflows.
- The entire stack is gated on the theme's predictive-search setting, which is also the switch that loads the templating library and the two scripts into the layout.
Edge Cases
- Aborted requests are recognized by their error code, so replacing an in-flight request by typing another character is not treated as a failure.
- The live region announces counts per group and a spoken no-results message, so the panel is legible to a screen reader without sight of the dropdown.
- Arrow-key navigation walks only options with a non-null
offsetParent, so a hidden group is skipped instead of trapping selection in an invisible row. - Focusout closes the panel only when focus has genuinely left the element, so clicking inside the results does not dismiss them.
Platform Primitives Used
- Predictive Search API — the element, the panel contract and the theme setting that gates it all come from Shopify's predictive-search stack; the index behind it does not.
- Custom elements — the whole build is a subclass of a theme custom element, which is why the lifecycle and accessibility behavior survived a change of data source.
- Liquid section schema and settings — carries the merchandising slots and the copy inside the templated panel as theme-editor settings.
Integrations in Play
- Handlebars — compiles the panel markup in the browser; the theme ships the build that carries the delimiter helper this template depends on.
Where It Runs
This is not a template-scoped build. The element mounts wherever the theme renders a search form — the header modal on every page, and the search results page, both hosting the same template holder — and the entire stack is behind one theme setting. It is the storefront's type-ahead, everywhere it appears, or it is nothing.
What This Demonstrates
Primary: On-site search, predictive search and external search services — the deepest instance of it in the corpus, because here the index is not Shopify's and the panel is still theme code. It is also the clearest example of a pattern this storefront uses twice: keep the Shopify component, keep its accessibility contract, and change only where the data comes from.
How We Know
Two artifacts carry this build — the predictive-search script and the Liquid template it compiles, roughly 350 lines between them — read out of the live theme along with the layout and header files that load them. Behind those sit twenty-two client working records: eight task-register entries covering the search program from build through styling to analytics, and fourteen specification documents describing search behavior in detail, down to how partial and single-character queries should resolve. The specifications do not name the storefront, so we read the code as the primary record and the documents as corroboration. What we can verify is what the theme sends and what it does with the answer. The service on the other end is not ours to describe, and this page does not.
Related Builds
- Tabbed multi-type search results page with faceted filtering — the results page on another storefront, where the type-ahead stayed on Shopify's index and the work went into the page a shopper lands on.
- Shade-level collection grid hydrated from an external faceting service — the same storefront's collection grid, built on the same division of labour between theme and service.
The Buy-vs-Build Question
Moving search off Shopify is one of the largest decisions a storefront makes, and it is usually made for ranking rather than for presentation. What that trade is actually worth, and what it obliges a brand to own afterwards, is set out in the site search decision.
Thinking About Moving Search Off Shopify?
It is a bigger decision than it looks, and the part everyone underestimates is the panel. Contact us today and let's map what you would be taking on.