Stable selectors
Hooks for finding the theme’s elements from an app or custom script — to place a wishlist button on product cards, add a message to the cart drawer, or track clicks on the cart icon.
The theme is styled with Tailwind utility classes such as flex gap-4 rounded-content. Those describe how an element looks, not what it is: they repeat across unrelated elements and change whenever the design does, so don’t target them. Instead, the elements on this page carry hook classes and data attributes that exist only for your code.
- Only what’s listed here is stable. Utility classes,
comp-*classes, otherdata-*attributes and the nesting of elements can all change in any update. - Hook classes are unstyled. The theme never styles them or uses them in its own scripts, so you’re free to style them from your own CSS.
- Names follow BEM:
block,block__element,block--modifier.
Pair these with theme events, which tell you when the theme has just changed the page.
Product card
Section titled “Product card”Every product card, wherever it appears: collection and search grids, carousels, product recommendations, predictive search and cart drawer upsells.
| Selector | Element |
|---|---|
.product-card |
The card |
.product-card__media |
The image area, position: relative |
.product-card__badges |
The badge row in the image’s top-right corner |
.product-card__vendor |
The vendor label in the image’s top-left corner |
.product-card__quick-add |
The quick add button’s wrapper, shown on hover |
.product-card__info |
The title and price |
.product-card__title |
The title link |
.product-card__price |
The price |
.product-card__swatches |
The color swatches |
Some parts only render when their setting is on, under Theme settings → Product card:
__badgesneeds Show badges.__vendorneeds Show vendor.__quick-addneeds Show quick add button. It never appears in search results or predictive search, and is hidden on touch devices.__swatchesneeds Show swatches, and the product needs an option named in Theme settings → Color swatches.
.product-card carries the product it shows:
| Attribute | |
|---|---|
data-product-id |
the product ID |
data-product-handle |
the product handle |
data-variant-id |
the selected or first available variant — the one quick add adds. Swatches don’t change it |
The whole of .product-card__media is covered by a link to the product page, and the theme layers its own elements up to z-index: 40. Anything clickable you place there needs position: absolute and a z-index of 50 or more:
for (const card of document.querySelectorAll(".product-card")) { const button = document.createElement("button"); button.className = "my-wishlist-button"; button.dataset.productId = card.dataset.productId; card.querySelector(".product-card__media").append(button);}.my-wishlist-button { position: absolute; top: 0.75rem; left: 0.75rem; z-index: 50;}Product grids
Section titled “Product grids”.product-grid marks every list of product cards. A modifier says which list it is:
| Selector | List |
|---|---|
.product-grid--collection |
The main grid on a collection page |
.product-grid--search |
The results on the search page. Article and page results share the grid |
.product-grid--predictive-search |
Product results in the search drawer |
.product-grid--recommendations |
The product recommendations section |
.product-grid--cart-upsell |
Upsell products beside the cart drawer |
.product-grid--featured |
Lists the merchant picks: products carousel, products grid, products with content, collection tabs, product grid and product custom grid |
Don’t rely on cards being direct children of the grid. In carousels each card sits inside a slide, so select .product-grid .product-card.
Filtering, sorting and pagination on collection and search pages all load a new page, so the cards present at load are the full set for that page.
Cards that arrive later
Section titled “Cards that arrive later”Two lists render after the page has loaded: product recommendations load as the section scrolls into view, and predictive search results arrive as the customer types. Code that decorates cards once on load won’t see them, so watch for new ones instead:
function decorate() { for (const card of document.querySelectorAll(".product-card:not([data-my-app])")) { card.dataset.myApp = ""; // add your button, stars, label… }}
decorate();new MutationObserver(decorate).observe(document.body, { childList: true, subtree: true });Marking each card as you go, as data-my-app does here, stops a card from being decorated twice.
The cart drawer and the cart page share the same hooks. .cart--drawer or .cart--page tells them apart.
| Selector | Element |
|---|---|
.cart |
The cart |
.cart__header |
The top of the cart: the heading and free shipping bar in the drawer, the title on the page |
.cart__items |
The line items. Absent when the cart is empty |
.cart__empty |
The empty cart message. Present only when the cart is empty |
.cart-item |
One line item |
.cart-item__media |
The line’s image |
.cart-item__info |
The title, options, selling plan, price and discounts |
.cart-item__title |
The title link |
.cart-item__price |
The line price |
.cart__footer |
The order note, discount code (drawer only) and totals |
.cart__subtotal |
The subtotal row |
.cart__checkout |
The <form> holding the checkout button, button[name="checkout"] |
.cart-item carries the line it shows:
| Attribute | |
|---|---|
data-cart-line-key |
the line item key |
data-product-id |
the product ID |
data-variant-id |
the variant ID |
Upsell products sit beside the drawer, not inside .cart. Find them with .product-grid--cart-upsell.
Re-attaching after a cart change
Section titled “Re-attaching after a cart change”Whenever the cart changes, everything inside .cart is replaced with fresh markup. The .cart element itself stays. Anything you inject inside it is lost, so add it once on load and again on theme:cart:updated (see theme events):
function addGiftNote() { const footer = document.querySelector(".cart--drawer .cart__footer"); if (!footer || footer.querySelector(".my-gift-note")) return; footer.insertAdjacentHTML("afterbegin", '<p class="my-gift-note">Free gift wrap on every order</p>');}
addGiftNote();theme.on("cart:updated", addGiftNote);Main product
Section titled “Main product”The product page’s main section, and any featured product section.
| Selector | Element |
|---|---|
.product-main |
The product |
.product-main--featured |
Added when it’s a featured product section rather than the product page |
.product-main__media |
The media gallery |
.product-main__info |
The column of blocks beside the gallery, including any app blocks |
.product-main__title |
The <h1> title |
.product-main__price |
The price. Updates in place when the variant changes |
.product-main__form |
The variant picker, quantity and add to cart button |
__title, __price and __form belong to blocks the merchant can remove or reorder, so check that they exist before using them.
.product-main carries data-product-id and data-product-handle.
If what you’re adding is a widget for the product page, an app block is better than injecting it: the merchant can place it anywhere in the info column from the theme editor.
The product form
Section titled “The product form”Inside .product-main__form is a standard Shopify product form, form[action*="/cart/add"]:
input[name="id"]holds the selected variant ID and fires achangeevent whenever the variant changes. Listen for it to keep a widget in step with the selection.button[name="add"]is the add to cart button.- Inputs you add inside the form named
properties[Name]are sent as line item properties. Blank values are left out. input[name="selling_plan"]is sent as the line’s selling plan. Blank means a one-time purchase.
The theme has its own purchase options for products sold on selling plans: a one-time purchase and each selling plan group, written to a hidden input[name="selling_plan"] that fires change whenever the plan changes. If your app adds its own selling_plan input instead, ask the merchant to turn off Show purchase options on the product form block and in the quick view settings, so there’s only one.
const form = document.querySelector('.product-main:not(.product-main--featured) form[action*="/cart/add"]');form?.querySelector('input[name="id"]').addEventListener("change", (event) => { console.log("variant", event.target.value);});Header
Section titled “Header”| Selector | Element |
|---|---|
.header |
The <header> |
.header__icons |
The icon group: search, cart, account and the mobile menu button |
.header__search |
The search icon, which opens the search drawer |
.header__cart |
The cart icon, a link to /cart that opens the cart drawer |
.header__cart-bubble |
The dot on the cart icon, shown while the cart has items. It’s a dot, not a count |
.header__search and .header__cart are only present when the header’s Show search icon and Show cart icon settings are on.