Skip to content

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, other data-* 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.

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:

  • __badges needs Show badges.
  • __vendor needs Show vendor.
  • __quick-add needs Show quick add button. It never appears in search results or predictive search, and is hidden on touch devices.
  • __swatches needs 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-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.

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.

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);

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.

Inside .product-main__form is a standard Shopify product form, form[action*="/cart/add"]:

  • input[name="id"] holds the selected variant ID and fires a change event 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);
});
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.