Customize with code
Relvello's collection and search results can be restyled with CSS, drawn with your own card markup, controlled from JavaScript, or used headless through the storefront API. Everything here works on top of the theme blocks, so Shopify's own results still show if Relvello can't be reached.
Pick a level
- Theme editor
- Block settings: layout, image corners, title size, badges, hover image, badge and button colors. No code. Block settings
- CSS
- Stable class names and custom properties. Styling
- Card markup
- Your own card, from a template in your theme or a JavaScript function. Cards
- JavaScript
window.Relvello: methods, events and hooks. JavaScript API- Headless
- The storefront API, for theme code that draws everything itself, Hydrogen, Next.js or an app. Storefront API
Styling
Every element Relvello draws has a class starting with rlv-. Those names, and the custom properties
below, are a contract: they won't change without notice. Set the properties on .rlv (or
:root) in your theme's CSS rather than overriding Relvello's rules.
--rlv-columns,--rlv-columns-mobile: grid columns (set by the block's settings).--rlv-gap,--rlv-gutter: space between cards, and the block's side padding.--rlv-ratio: image shape, e.g.3 / 4.--rlv-radius,--rlv-card-radius: corner rounding for buttons and fields, and for card images.--rlv-title-size: product title size.--rlv-badge-fg,--rlv-badge-bg,--rlv-sale-fg,--rlv-sale-bg: sold-out and sale badge colors.--rlv-button-fg,--rlv-button-bg: primary buttons (default: the section's own colors).--rlv-line,--rlv-soft,--rlv-muted: borders, light fills and secondary text.
.rlv {
--rlv-card-radius: 0;
--rlv-sale-bg: #b42318;
--rlv-title-size: 1rem;
}
.rlv-card__title { text-transform: uppercase; letter-spacing: .04em; }
Your own product cards
Put a <template data-relvello-card> inside the block's section, or one with
id="relvello-card" anywhere in the theme for every block. Relvello fills a copy for each product
using attributes, so the template is plain HTML (no Liquid conflicts):
data-rlv-text="title": sets the element's text.data-rlv-attr="href:url, alt:title": sets attributes, asname:fieldpairs.data-rlv-if="on_sale"/data-rlv-if="!available": keeps the element only when the field is true (or false).
<template id="relvello-card">
<a class="card" data-rlv-attr="href:url">
<img data-rlv-attr="src:image_url, srcset:image_srcset, alt:image_alt" loading="lazy" sizes="25vw">
<span class="badge" data-rlv-if="on_sale" data-rlv-text="sale_label"></span>
<h3 data-rlv-text="title"></h3>
<p><span data-rlv-text="price"></span> <s data-rlv-text="compare_at_price"></s></p>
</a>
</template>
Fields: id, key, handle, title, vendor, type,
url, option (the color or variant on a split card), price (with "From" for a range),
price_min, compare_at_price, on_sale, available, sold_out,
has_image, image_url, image_srcset, image_alt, image2_url,
sale_label, sold_out_label.
Values always go in as text, never as HTML. Event handler attributes (on…), style and
srcdoc can't be set, and links and image sources only take http(s) or same-site URLs.
Each card is wrapped in <li class="rlv-card rlv-card--custom">, which keeps analytics and
"Load more" working.
JavaScript API
window.Relvello is ready when the block's script has run; listen for relvello:ready on
document if your code may run first.
const results = Relvello.get('[data-relvello]'); // or Relvello.all()
results.state(); // { mode, handle, query, total, hasMore, products, facets, params, live }
results.setFilters({ 'filter.p.vendor': ['Kestrel'], 'filter.v.price.lte': 80 });
results.setSort('price-ascending');
results.clearFilters();
results.loadMore();
results.refresh();
setFilters takes an object, a query string or URLSearchParams. Only sort_by
and filter.* parameters are used; anything else is ignored.
Events
Dispatched on the block and bubbling, so Relvello.on(type, listener) (or a listener on
document) hears every block. event.detail holds copies: changing them changes nothing.
rlv:ready- A block started.
{ mode, handle, query } rlv:before-load- About to fetch results.
{ append, params } rlv:results- Results arrived.
{ mode, handle, query, products, facets, total, append } rlv:render- The grid was updated.
{ total, append } rlv:filter-change- Sort or filters changed.
{ params } rlv:columns- A shopper chose cards per row.
{ columns, mobile } rlv:product-click- A card was clicked.
{ product, position } rlv:error- Results couldn't load.
{ message, live }(not live: Shopify's results stay).
Relvello.on('rlv:product-click', (event) => {
const { product, position } = event.detail;
dataLayer.push({ event: 'select_item', item_id: product.id, index: position });
});
Hooks
Set them for every block with Relvello.configure (or window.RelvelloConfig before
Relvello loads), or for one block with Relvello.get(el).configure.
window.RelvelloConfig = {
// Change product data before it's drawn (return the product).
transformProduct: (product) => ({ ...product, title: product.title.replace(/ - .*$/, '') }),
// Draw a card: return HTML, a DOM node, or null for the default card.
renderCard: (product, { fields, esc, defaultCard }) =>
`<a href="${esc(fields.url)}">${esc(fields.title)} · ${esc(fields.price)}</a>`,
};
renderCard is inserted as written, so
pass every value through esc (or build DOM nodes). Card templates don't need this: they never
insert HTML. If a hook throws, that card falls back to Relvello's own.
Placing Relvello from theme code
Turn on Relvello for theme code under Customize › App embeds. It loads Relvello on every page, so any template or section can hold a block; only a grid is required:
<div data-relvello data-handle="{{ collection.handle }}" data-per-page="12">
<ul class="rlv__grid" data-rlv-grid></ul>
<button type="button" class="rlv-button" data-rlv-more hidden>Load more</button>
</div>
For search results, use data-mode="search" data-query="". Server-render
Shopify's own products in the grid too if you want a fallback for search engines and outages.
Storefront API
A public, read-only JSON API with the same results shoppers see. It needs no key and returns nothing that isn't already public on your storefront. Find your store's endpoint under Organization › API:
GET https://edge.relvello.com/s/<store>/v1/collections/<handle>/products?sort_by&filter.*&limit&cursor
GET https://edge.relvello.com/s/<store>/v1/collections/<handle>/filters
GET https://edge.relvello.com/s/<store>/v1/search?q&sort_by&filter.*&limit&cursor
GET https://edge.relvello.com/s/<store>/v1/search/suggest?q&limit
GET https://edge.relvello.com/s/<store>/v1/recommendations?product&kind&ids&limit
Results carry products, facets, total and next_cursor (pass it as
cursor for the next page). A search with a redirect rule carries redirect. Parameters follow
Shopify's: sort_by=price-ascending, filter.p.vendor=Kestrel,
filter.v.price.gte=20. Errors answer { "ok": false, "error": "…" }.
Products can carry a badge ({ label, style }, from your
badges). Collection and search pages carry promos: your
promo tiles on that page, each with after, the number of
the page's products that come before it. Suggestions carry pages ({ title, url }).
recommendations takes kind=bought, similar or viewed (with
ids, the shopper's recently viewed product ids, newest first).
With personalization on, collection requests can say who the
shopper is: v=n or v=r (new or returning), mk (the Shopify Markets country, like
US) and aff (up to three product types they clicked lately, comma-separated). The answer
carries segment when one applied.
During an A/B test, collection results follow the shopper's group:
pass ab=a or ab=b and the answer carries experiment: { id, arm }. Relvello's
blocks send the group they keep in localStorage (rlv_ab, set only with analytics consent);
your own code can read and pass the same value. Without ab you get version A.
const api = Relvello.client(); // on your storefront (the page's own store)
// const api = Relvello.client({ store: 'your-store' }); // elsewhere
const page = await api.collection('new-arrivals', { limit: 12, sort_by: 'best-selling' });
const found = await api.search('linen shirt', { 'filter.p.product_type': 'Shirts' });
const typed = await api.suggest('lin');
Which websites may call it
Browsers may call the API only from your store's own domains. To call it from another site (a headless storefront,
staging, http://localhost:3000 while developing), add the site under
Organization › API › Storefront API and code, or with the API's
set_storefront_origins. Use exact origins only: no paths, no wildcards, and plain http only for localhost.
Server-side calls (from your own backend) need no entry.
Limits and usage
- Each call counts as a storefront request toward your plan, like a collection load or a search on the theme.
- Requests are rate limited per store. Over the limit, the API answers
429; show Shopify's own results or try again shortly. - Responses are cached at the edge for about a minute, so changes in the dashboard can take that long to appear.
Security
- Nothing private is on the page: the embed and blocks only carry the public endpoint, never a key.
- Changing what the storefront shows (rules, pins, synonyms) needs a signed-in owner or admin, or a read-and-write key for the REST API and MCP server (Organization › API).
- Card templates insert text only, and query rule redirects only go to
http(s)pages.