Filter UIs
Where option values come from
filter_facets lists the field names enabled for your account. It carries
no values and no counts:
-
sizeis server-counted. Sizes live on variants and results are grouped by product, so they cannot be read off the product cards. When the facet is enabled the backend sends them on their own stream key, pre-sorted, and scoped so that selecting one size does not remove the others: - Every other field is derived by you, from the products in the response.
A facet must never narrow its own option list
Saygender offers female and male. The shopper picks male, you send the
filter, and the response now contains only men’s products — so the only gender
value you can derive is male. Rebuild the control from that and female
disappears. The multi-select has become a one-way door: the shopper cannot get
back, cannot select both, and the group looks broken.
Cross-facet narrowing is the opposite case and it is correct. If the shopper
filters by gender, the material options should shrink to the materials
men’s products actually come in. Only a facet’s own filter must be excluded from
its own options.
The fix is to remember what each field offered before it was filtered, and
re-offer those values at count 0 while that field has a selection:
keyword payload you receive, including later pages — more
products means more values observed, and the set only grows within one query.
Key the memory on everything that changes the vocabulary — the query, and also
locale and country. Attribute values are localised, so the same field
returns different words per market; a market switch mid-session must start the
memory over rather than merge two languages into one control.
The memory only ever re-offers values your catalogue actually returned in this
locale, which is why it is a memory and not a hardcoded list.
Then build the options for one field:
This approximates a disjunctive facet rather than implementing one. A true
disjunctive count for
gender would be computed over “every filter except
gender”, which the client cannot see — it only ever receives products
matching all filters. So a re-offered value can occasionally be an option
that yields nothing once a second field is also filtered. That is the right
trade: an option that returns no results is recoverable, an option that has
vanished is not.Keep a selected value visible, even at count 0
A selected value must stay in the list whatever the counts say. It is not just an option — it is the control that clears the filter. Drop it and the shopper is holding an active filter with nothing on screen to switch it off, and the only way out is a global “clear all”. This is why the snippet above seedsselected separately from knownValues:
the memory is only consulted while the field has a selection, but the selection
is always re-added.
Zero-count options must stay clickable
Greying out or disabling a zero-count option looks tidy and breaks the control. Consider a single-select-stylegender facet with male selected. In the
men-filtered results female has count 0. Disable it, and the shopper can never
switch to female — the only enabled option is the one already selected. The
facet is stuck on its first choice for the rest of the session.
Render zero-count options as normal, clickable options. Clicking one should do
what clicking any option does: add it to the selection, or swap the selection if
your control is single-select. If you want to signal that an option is thinly
populated, do it with styling that does not remove the click target.
Optional refinements
- Hide an empty group. A field whose derived options are empty and which has no selection can be hidden — there is nothing to choose. Do not hide a group that has a selection, for the reason above.
- Seed closed vocabularies. Some fields have a small, fixed set of values —
genderis usuallyfemale/male, sometimes withunisex. If you know the full set for your catalogue, you can seed the control statically and give each value a display label (“Women”, “Men”) instead of showing the raw indexed value. Two cautions: the values are localised, so a static list is only safe for the locales you have actually checked, and the value you send in a filter must still be the exact indexed value, not the label. - Do not re-sort server-counted options. They arrive in display order.
Sending the filter
Each entry is{ field, operator, value }, and the values are echoed back
verbatim from what you derived:
in with an array. Full operator and field list, and
the rules about which fields are safe to filter on, are in the
API reference.
Changing a filter is a new search: re-run
/search_v2 with a fresh
search_id, debounce rapid toggles, and cancel the in-flight request when a
new one starts.Rendering query suggestions
The format
/autocomplete_v2 returns suggestions in a markdown-lite form where ** marks
the completion — the part the shopper has not typed yet:
** is what you bold. Split on
**, escape every part, and wrap the odd-indexed parts:
Whitespace: the disappearing space
When the completion starts a new word, its leading space is inside the**
span. After parsing you have two sibling nodes, and the space is the first
character of the second one:
loafersin dark brown suede. The parsing is
correct; CSS ate the space.
Either set white-space: pre-wrap on the element that holds the suggestion
text, or do not make that element a flex container — put the flex layout on a
wrapper (icon, text, chevron) and let the text itself be a plain inline
container.
The same applies to any layout that reflows text nodes independently, not just
flex. If the space is present in your parsed output but missing on screen, it
is a CSS problem, not a parsing one.
Ordering
Suggestion responses are CDN-cached, so a response for an earlier keystroke can arrive after a newer one. Record which query each response belongs to and discard stale ones, or the list will flicker back to older completions as the shopper types.Next
API reference
Every endpoint, field, stream key and filter.
Tracking events
The events to send, and how attribution works.

