Replace our widget markup with your own, while the app keeps supplying the products and tracking the revenue. This page is a short overview of the three paths.
The three paths
Path | You write | The app still does |
|---|---|---|
Template card | An HTML | Rendering, clicks, add-to-cart, and revenue tracking. |
Custom card element | A web component for the card only. | The layout, loading, and impression tracking. |
Data-only mode | All the rendering, in your theme JS. | Fetches and prepares the product data. |
Template card (no JavaScript)
Put a <template data-pai-card> inside any widget container in your theme code. The app clones it once per recommended product and fills the placeholders. It also wires the click and add-to-cart tracking for you.
<div data-pai-widget="recommendations" data-pai-model="SIMILAR"
data-pai-product-id="{{ product.id }}" data-pai-limit="6">
<template data-pai-card>
<div class="my-card">
<a href="{{ url }}">
<img src="{{ image_url }}" alt="{{ image_alt }}">
<span>{{ title }}</span>
<span>{{ price }}</span>
</a>
<button data-pai-bind-action="add-to-cart"
data-pai-variant="{{ variant_id }}">Add to cart</button>
</div>
</template>
</div>For developers: set data-pai-model to a Custom Blend's slug instead of a built-in model name, for example data-pai-model="my-blend-slug". A custom blend needs a product anchor, so keep data-pai-product-id on the container. See Hand-picked & custom models.
You can bind these placeholders:
title,url,handle,idimage_url,image_altprice,compare_at_price,currencyvariant_idvendor
Prices arrive formatted, for example $19.99. Your template decides what it shows: the card settings in Design → Cards (vendor, rating, compare-at price) apply to the built-in card, not to a template. To hide the compare-at price, leave out compare_at_price.
Custom card element
Define a web component in your theme JS with customElements.define(). Then tell the app to render it instead of the built-in card. The app keeps its layouts and tracking; your component owns the card surface.
Sales are credited without any tracking code: the app records a click on a product link inside your card, and credits your card's own add to cart (a Shopify cart form or a /cart/add.js request) to the widget. Each card also gets the widget's details as attributes (data-widget-id, data-model, data-placement, data-request-id) for anything else you want to do with them. If your component hides its links in a closed shadow root, it has to report clicks itself; the contract shows how.
- For every widget: set it in Design → Advanced → Custom product card. It replaces your Code mode card template too.
- For one widget: add
data-pai-card-element="my-card"on that widget container.
To insert small extras into the built-in card instead — a wishlist button, a compare checkbox — use the slots in Design → Cards → Extra elements on each card (advanced): After image, After price, After card.
Data-only mode
Add data-pai-render="false" to a widget container. The app fetches the products but renders nothing. It dispatches a pai:data-ready event on the container, with the products in the event detail. Your theme JS listens and renders.
Which path wins?
A container can carry more than one of these at once. A template card wins over both a custom card element and data-only mode. Data-only mode wins over a custom card element.
The full contract
This page is only an overview. The complete contract — every attribute, every event payload, the attribution rules, and Hydrogen/headless setups — lives in the developer docs, in the repository file docs/widgets/byo-widget-contract.md ("Bring Your Own Widget — Contract"). Share that file with your developer or agency.
Related pages
- Widget catalog — the built-in widgets you are replacing.
- Style your widgets — restyle without code.
- API keys & headless — for storefronts outside Shopify themes.