← All sections

Quick-add Grid

A bestseller grid with hover image swap and one-tap add to cart.

FreeCollections20 settingsInstall
Full screen ↗

Install

One file: sections/mf-quick-add-grid.liquid. It appears in the theme editor as EX · Quick-add grid.

  1. Connect EXPERAISE Sections to Claude (once). In Claude Code:
    terminal
    claude mcp add --transport http experaise-sections https://sections.experaise.com/api/mcp
    In the Claude app: Settings → Connectors → Add custom connector → https://sections.experaise.com/api/mcp. Full setup
  2. Ask Claude. It fetches sections/mf-quick-add-grid.liquid, pushes only that file to your theme with Shopify CLI, and tells you where to find it.
    prompt
    Install the EXPERAISE Sections section "Quick-add Grid" (quick-add-grid) on my Shopify store your-store.myshopify.com. Put it on an unpublished copy of my live theme, then tell me how to add it in the theme editor.
  3. Customize. Online Store → Themes → Customize → Add section → EX · Quick-add grid. Every text, color and product is a setting.
sections/mf-quick-add-grid.liquidDownload
{%- liquid
  assign s = section.settings
  assign mf_collection = s.collection
  assign limit = s.products_to_show
-%}

<mf-quick-add-grid
  class="mf-qag mf-qag--{{ s.card_style }} mf-qag--mobile-{{ s.mobile_layout }}"
  data-after-add="{{ s.after_add }}"
  data-cart-url="{{ routes.cart_url }}"
  style="--mf-qag-bg: {{ s.color_bg }}; --mf-qag-fg: {{ s.color_text }}; --mf-qag-accent: {{ s.color_accent }}; --mf-qag-card: {{ s.color_card }}; --mf-qag-cols: {{ s.columns }}; --mf-qag-ratio: {{ s.image_ratio }}; --mf-qag-pt: {{ s.padding_top }}px; --mf-qag-pb: {{ s.padding_bottom }}px;"
>
  <div class="mf-qag__inner">
    <div class="mf-qag__head">
      <div>
        {%- if s.eyebrow != blank -%}
          <p class="mf-qag__eyebrow">{{ s.eyebrow | escape }}</p>
        {%- endif -%}
        {%- if s.heading != blank -%}
          <h2 class="mf-qag__title">{{ s.heading }}</h2>
        {%- endif -%}
      </div>
      {%- if s.view_all_label != blank -%}
        <a class="mf-qag__all" href="{% if mf_collection != blank %}{{ mf_collection.url }}{% else %}{{ routes.all_products_collection_url }}{% endif %}">
          {{- s.view_all_label | escape -}}
          <svg viewBox="0 0 20 20" width="18" height="18" aria-hidden="true"><path d="M4 10h11m-4-5 5 5-5 5" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>
        </a>
      {%- endif -%}
    </div>

    <ul class="mf-qag__grid" role="list">
      {%- if mf_collection != blank and mf_collection.products.size > 0 -%}
        {%- for card_product in mf_collection.products limit: limit -%}
          {%- liquid
            assign variant = card_product.selected_or_first_available_variant
            assign on_sale = false
            if card_product.compare_at_price > card_product.price
              assign on_sale = true
              assign saving = card_product.compare_at_price | minus: card_product.price | times: 100 | divided_by: card_product.compare_at_price
            endif
            assign second_image = card_product.media[1]
            assign rating = card_product.metafields.reviews.rating.value
            assign rating_count = card_product.metafields.reviews.rating_count.value
          -%}
          <li class="mf-qag__item" style="--mf-qag-i: {{ forloop.index0 }};">
            <article class="mf-qag__card{% unless card_product.available %} mf-qag__card--soldout{% endunless %}">
              <div class="mf-qag__media">
                <a href="{{ card_product.url }}" class="mf-qag__media-link" tabindex="-1" aria-hidden="true">
                  {%- if card_product.featured_media != blank -%}
                    {{
                      card_product.featured_media
                      | image_url: width: 900
                      | image_tag:
                        loading: 'lazy',
                        sizes: '(min-width: 990px) 25vw, (min-width: 750px) 33vw, 50vw',
                        widths: '360, 540, 720, 900',
                        class: 'mf-qag__img mf-qag__img--primary'
                    }}
                    {%- if s.show_second_image and second_image != blank -%}
                      {{
                        second_image
                        | image_url: width: 900
                        | image_tag:
                          loading: 'lazy',
                          sizes: '(min-width: 990px) 25vw, (min-width: 750px) 33vw, 50vw',
                          widths: '360, 540, 720, 900',
                          class: 'mf-qag__img mf-qag__img--secondary',
                          alt: ''
                      }}
                    {%- endif -%}
                  {%- else -%}
                    {{ 'product-1' | placeholder_svg_tag: 'mf-qag__img mf-qag__img--primary' }}
                  {%- endif -%}
                </a>

                <div class="mf-qag__badges">
                  {%- if card_product.available == false -%}
                    <span class="mf-qag__badge mf-qag__badge--muted">Sold out</span>
                  {%- elsif on_sale -%}
                    <span class="mf-qag__badge mf-qag__badge--sale">−{{ saving }}%</span>
                  {%- elsif card_product.tags contains 'new' -%}
                    <span class="mf-qag__badge">New</span>
                  {%- elsif card_product.tags contains 'bestseller' -%}
                    <span class="mf-qag__badge">Bestseller</span>
                  {%- endif -%}
                </div>

                {%- if card_product.available -%}
                  <div class="mf-qag__quick">
                    {%- if card_product.has_only_default_variant -%}
                      {%- form 'product', card_product, class: 'mf-qag__form' -%}
                        <input type="hidden" name="id" value="{{ variant.id }}">
                        <input type="hidden" name="quantity" value="1">
                        <button type="submit" name="add" class="mf-qag__add" data-mf-add>
                          <span class="mf-qag__add-label">{{ s.quick_add_label | escape }}</span>
                          <span class="mf-qag__add-done" aria-hidden="true">{{ s.added_label | escape }}</span>
                          <svg class="mf-qag__plus" viewBox="0 0 20 20" width="16" height="16" aria-hidden="true"><path d="M10 4v12M4 10h12" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/></svg>
                        </button>
                      {%- endform -%}
                    {%- else -%}
                      <div class="mf-qag__chips" role="group" aria-label="{{ s.quick_add_label | escape }}: {{ card_product.title | escape }}">
                        {%- for v in card_product.variants limit: 5 -%}
                          <button
                            type="button"
                            class="mf-qag__chip"
                            data-mf-variant="{{ v.id }}"
                            aria-label="{{ s.quick_add_label | escape }} {{ v.title | escape }}"
                            {% unless v.available %}disabled{% endunless %}
                          >
                            {{- v.title | escape -}}
                          </button>
                        {%- endfor -%}
                      </div>
                    {%- endif -%}
                  </div>
                {%- endif -%}
              </div>

              <div class="mf-qag__info">
                {%- if s.show_rating and rating != blank -%}
                  {%- assign rating_pct = rating.rating | divided_by: rating.scale_max | times: 100 -%}
                  <p class="mf-qag__rating">
                    <span class="mf-qag__stars" style="--mf-qag-rating: {{ rating_pct }}%;" aria-hidden="true"></span>
                    <span class="mf-qag__visually-hidden">{{ rating.rating }} / {{ rating.scale_max }}</span>
                    {%- if rating_count != blank -%}<span class="mf-qag__count">({{ rating_count }})</span>{%- endif -%}
                  </p>
                {%- endif -%}
                <h3 class="mf-qag__name">
                  <a href="{{ card_product.url }}">{{ card_product.title | escape }}</a>
                </h3>
                <p class="mf-qag__price">
                  {%- if card_product.price_varies -%}
                    <span>From {{ card_product.price_min | money }}</span>
                  {%- else -%}
                    <span{% if on_sale %} class="mf-qag__price--sale"{% endif %}>{{ card_product.price | money }}</span>
                  {%- endif -%}
                  {%- if on_sale -%}
                    <s class="mf-qag__compare">{{ card_product.compare_at_price | money }}</s>
                  {%- endif -%}
                </p>
              </div>
            </article>
          </li>
        {%- endfor -%}
      {%- else -%}
        {%- for i in (1..4) -%}
          <li class="mf-qag__item" style="--mf-qag-i: {{ forloop.index0 }};">
            <article class="mf-qag__card">
              <div class="mf-qag__media">
                {%- capture placeholder_name -%}product-{{ i }}{%- endcapture -%}
                {{ placeholder_name | placeholder_svg_tag: 'mf-qag__img mf-qag__img--primary' }}
              </div>
              <div class="mf-qag__info">
                <h3 class="mf-qag__name">Your product</h3>
                <p class="mf-qag__price"><span>{{ 3900 | money }}</span></p>
              </div>
            </article>
          </li>
        {%- endfor -%}
      {%- endif -%}
    </ul>
  </div>
  <p class="mf-qag__visually-hidden" role="status" aria-live="polite" data-mf-status></p>
</mf-quick-add-grid>

{% stylesheet %}
  .mf-qag {
    display: block;
    background: var(--mf-qag-bg, #f6f3ee);
    color: var(--mf-qag-fg, #17140f);
    padding: var(--mf-qag-pt, 72px) 0 var(--mf-qag-pb, 72px);
    font-family: inherit;
  }
  .mf-qag *,
  .mf-qag *::before,
  .mf-qag *::after {
    box-sizing: border-box;
  }
  .mf-qag__inner {
    width: min(100% - 40px, 1320px);
    margin: 0 auto;
  }
  .mf-qag__head {
    display: flex;
    align-items: flex-end;
    justify-content: space-between;
    gap: 24px;
    margin-bottom: 34px;
  }
  .mf-qag__eyebrow {
    margin: 0 0 10px;
    font-size: 12px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--mf-qag-accent);
  }
  .mf-qag__title {
    margin: 0;
    font-family: inherit;
    font-size: clamp(30px, 4vw, 52px);
    font-weight: 600;
    line-height: 1;
    letter-spacing: -0.035em;
    color: inherit;
  }
  .mf-qag__title em {
    font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif;
    font-weight: 400;
  }
  .mf-qag__all {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    flex-shrink: 0;
    padding: 10px 0;
    color: inherit;
    font-size: 14px;
    font-weight: 600;
    text-decoration: none;
    border-bottom: 1px solid currentColor;
  }
  .mf-qag__all svg {
    transition: transform 0.25s ease;
  }
  .mf-qag__all:hover svg {
    transform: translateX(4px);
  }
  .mf-qag__grid {
    display: grid;
    grid-template-columns: repeat(var(--mf-qag-cols, 4), minmax(0, 1fr));
    gap: 22px;
    margin: 0;
    padding: 0;
    list-style: none;
  }
  .mf-qag__item {
    min-width: 0;
  }
  .mf-qag--js .mf-qag__item {
    opacity: 0;
    transform: translateY(26px);
    transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.2, 0.7, 0.2, 1);
    transition-delay: calc(var(--mf-qag-i, 0) * 70ms);
  }
  .mf-qag--js.mf-qag--in .mf-qag__item {
    opacity: 1;
    transform: none;
  }
  .mf-qag__card {
    position: relative;
    height: 100%;
  }
  .mf-qag__media {
    position: relative;
    overflow: hidden;
    aspect-ratio: var(--mf-qag-ratio, 0.8);
    border-radius: 18px;
    background: var(--mf-qag-card, #ebe5dc);
    isolation: isolate;
  }
  .mf-qag--elevated .mf-qag__media {
    box-shadow: 0 1px 0 rgba(0, 0, 0, 0.04), 0 18px 40px -24px rgba(40, 28, 10, 0.45);
  }
  .mf-qag__media-link {
    position: absolute;
    inset: 0;
    display: block;
  }
  .mf-qag__img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: opacity 0.6s ease, transform 1.2s cubic-bezier(0.2, 0.7, 0.2, 1);
  }
  .mf-qag__img--secondary {
    opacity: 0;
  }
  .mf-qag__card:hover .mf-qag__img--primary {
    transform: scale(1.05);
  }
  .mf-qag__card:hover .mf-qag__img--secondary {
    opacity: 1;
    transform: scale(1.03);
  }
  .mf-qag__card--soldout .mf-qag__img {
    filter: grayscale(0.6);
    opacity: 0.7;
  }
  .mf-qag__badges {
    position: absolute;
    z-index: 2;
    top: 14px;
    left: 14px;
    display: flex;
    gap: 6px;
    pointer-events: none;
  }
  .mf-qag__badge {
    display: inline-flex;
    align-items: center;
    min-height: 26px;
    padding: 0 11px;
    border-radius: 999px;
    background: rgba(255, 255, 255, 0.86);
    color: #17140f;
    font-size: 11px;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
  }
  .mf-qag__badge--sale {
    background: var(--mf-qag-accent);
    color: #fff;
  }
  .mf-qag__badge--muted {
    background: rgba(23, 20, 15, 0.78);
    color: #fff;
  }
  .mf-qag__quick {
    position: absolute;
    z-index: 3;
    left: 12px;
    right: 12px;
    bottom: 12px;
    transform: translateY(calc(100% + 16px));
    transition: transform 0.45s cubic-bezier(0.2, 0.7, 0.2, 1);
  }
  .mf-qag__card:hover .mf-qag__quick,
  .mf-qag__card:focus-within .mf-qag__quick {
    transform: none;
  }
  .mf-qag__form {
    margin: 0;
  }
  .mf-qag__add {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    width: 100%;
    min-height: 46px;
    padding: 0 18px;
    border: 0;
    border-radius: 12px;
    background: rgba(255, 255, 255, 0.92);
    color: #17140f;
    font: inherit;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    overflow: hidden;
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.35);
    transition: background 0.25s ease, color 0.25s ease;
  }
  .mf-qag__add:hover {
    background: #17140f;
    color: #fff;
  }
  .mf-qag__add-done {
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    background: var(--mf-qag-accent);
    color: #fff;
    transform: translateY(100%);
    transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1);
  }
  .mf-qag__add[data-state="loading"] {
    pointer-events: none;
  }
  .mf-qag__add[data-state="loading"] .mf-qag__plus {
    animation: mf-qag-spin 0.8s linear infinite;
  }
  .mf-qag__add[data-state="done"] .mf-qag__add-done {
    transform: none;
  }
  .mf-qag__chips {
    display: flex;
    flex-wrap: wrap;
    gap: 6px;
    padding: 8px;
    border-radius: 14px;
    background: rgba(255, 255, 255, 0.9);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    box-shadow: 0 10px 30px -12px rgba(0, 0, 0, 0.35);
  }
  .mf-qag__chip {
    flex: 1 1 auto;
    min-height: 36px;
    padding: 0 12px;
    border: 1px solid rgba(23, 20, 15, 0.16);
    border-radius: 9px;
    background: transparent;
    color: #17140f;
    font: inherit;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
  }
  .mf-qag__chip:hover {
    background: #17140f;
    border-color: #17140f;
    color: #fff;
  }
  .mf-qag__chip[disabled] {
    opacity: 0.4;
    text-decoration: line-through;
    cursor: not-allowed;
  }
  .mf-qag__chip[data-state="done"] {
    background: var(--mf-qag-accent);
    border-color: var(--mf-qag-accent);
    color: #fff;
  }
  .mf-qag__add:focus-visible,
  .mf-qag__chip:focus-visible,
  .mf-qag__all:focus-visible,
  .mf-qag__name a:focus-visible {
    outline: 2px solid var(--mf-qag-accent);
    outline-offset: 3px;
  }
  .mf-qag__info {
    padding: 16px 4px 0;
  }
  .mf-qag__rating {
    display: flex;
    align-items: center;
    gap: 6px;
    margin: 0 0 6px;
    font-size: 12px;
    opacity: 0.8;
  }
  .mf-qag__stars {
    position: relative;
    display: inline-block;
    width: 74px;
    height: 13px;
    background: linear-gradient(90deg, var(--mf-qag-fg) var(--mf-qag-rating, 100%), rgba(0, 0, 0, 0.16) var(--mf-qag-rating, 100%));
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 14'%3E%3Cpath d='M7.5 0l2.2 4.6 5 .6-3.7 3.4 1 5-4.5-2.5L3 13.6l1-5L.3 5.2l5-.6z'/%3E%3C/svg%3E") repeat-x left center / 15px 13px;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 14'%3E%3Cpath d='M7.5 0l2.2 4.6 5 .6-3.7 3.4 1 5-4.5-2.5L3 13.6l1-5L.3 5.2l5-.6z'/%3E%3C/svg%3E") repeat-x left center / 15px 13px;
  }
  .mf-qag__name {
    margin: 0;
    font-family: inherit;
    font-size: 16px;
    font-weight: 600;
    line-height: 1.3;
    letter-spacing: -0.01em;
  }
  .mf-qag__name a {
    color: inherit;
    text-decoration: none;
  }
  .mf-qag__name a::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 1;
  }
  .mf-qag__price {
    display: flex;
    align-items: baseline;
    gap: 8px;
    margin: 6px 0 0;
    font-size: 15px;
  }
  .mf-qag__price--sale {
    color: var(--mf-qag-accent);
    font-weight: 700;
  }
  .mf-qag__compare {
    font-size: 13px;
    opacity: 0.55;
  }
  .mf-qag__visually-hidden {
    position: absolute !important;
    width: 1px;
    height: 1px;
    margin: -1px;
    padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
  }
  @keyframes mf-qag-spin {
    to { transform: rotate(360deg); }
  }
  @media (hover: none) {
    .mf-qag__quick {
      transform: none;
    }
  }
  @media screen and (max-width: 989px) {
    .mf-qag__grid {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
  }
  @media screen and (max-width: 749px) {
    .mf-qag__head {
      margin-bottom: 24px;
    }
    .mf-qag--mobile-grid .mf-qag__grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
      gap: 14px;
    }
    .mf-qag--mobile-carousel .mf-qag__grid {
      display: flex;
      gap: 14px;
      margin: 0 -20px;
      padding: 0 20px 8px;
      overflow-x: auto;
      scroll-snap-type: x mandatory;
      scroll-padding: 0 20px;
      scrollbar-width: none;
    }
    .mf-qag--mobile-carousel .mf-qag__item {
      flex: 0 0 72%;
      scroll-snap-align: start;
    }
    .mf-qag__add {
      min-height: 40px;
      font-size: 13px;
    }
    .mf-qag__media {
      border-radius: 14px;
    }
  }
  @media (prefers-reduced-motion: reduce) {
    .mf-qag--js .mf-qag__item,
    .mf-qag__img,
    .mf-qag__quick,
    .mf-qag__add-done {
      transition: none;
    }
    .mf-qag--js .mf-qag__item {
      opacity: 1;
      transform: none;
    }
    .mf-qag__add[data-state="loading"] .mf-qag__plus {
      animation: none;
    }
  }
{% endstylesheet %}

{% javascript %}
  if (!customElements.get('mf-quick-add-grid')) {
    customElements.define(
      'mf-quick-add-grid',
      class extends HTMLElement {
        connectedCallback() {
          this.status = this.querySelector('[data-mf-status]');
          this.onClick = this.onClick.bind(this);
          this.onSubmit = this.onSubmit.bind(this);
          this.addEventListener('click', this.onClick);
          this.addEventListener('submit', this.onSubmit);

          const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
          if (!reduce && 'IntersectionObserver' in window) {
            this.classList.add('mf-qag--js');
            this.observer = new IntersectionObserver(
              (entries) => {
                if (entries.some((e) => e.isIntersecting)) {
                  this.classList.add('mf-qag--in');
                  this.observer.disconnect();
                }
              },
              { rootMargin: '0px 0px -10% 0px' }
            );
            this.observer.observe(this);
          }
        }

        disconnectedCallback() {
          this.removeEventListener('click', this.onClick);
          this.removeEventListener('submit', this.onSubmit);
          if (this.observer) this.observer.disconnect();
        }

        onSubmit(event) {
          const form = event.target.closest('.mf-qag__form');
          if (!form) return;
          event.preventDefault();
          const button = form.querySelector('[data-mf-add]');
          const id = form.querySelector('input[name="id"]').value;
          this.add(id, button);
        }

        onClick(event) {
          const chip = event.target.closest('[data-mf-variant]');
          if (!chip || chip.disabled) return;
          event.preventDefault();
          this.add(chip.dataset.mfVariant, chip);
        }

        add(variantId, button) {
          if (!button || button.dataset.state === 'loading') return;
          button.dataset.state = 'loading';
          const root = (window.Shopify && window.Shopify.routes && window.Shopify.routes.root) || '/';
          fetch(root + 'cart/add.js', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
            body: JSON.stringify({ items: [{ id: Number(variantId), quantity: 1 }] }),
          })
            .then((response) => {
              if (!response.ok) throw new Error('add failed');
              return response.json();
            })
            .then((data) => {
              button.dataset.state = 'done';
              if (this.status) this.status.textContent = button.textContent.trim() + ' ✓';
              document.dispatchEvent(new CustomEvent('cart:refresh', { bubbles: true }));
              document.dispatchEvent(new CustomEvent('mf:cart:added', { bubbles: true, detail: data }));
              if (this.dataset.afterAdd === 'cart' && !window.__MF_PREVIEW__) {
                window.location.href = this.dataset.cartUrl || '/cart';
                return;
              }
              setTimeout(() => {
                delete button.dataset.state;
              }, 1800);
            })
            .catch(() => {
              delete button.dataset.state;
              const form = button.closest('form');
              if (form && !window.__MF_PREVIEW__) form.submit();
            });
        }
      }
    );
  }
{% endjavascript %}

{% schema %}
{
  "name": "EX · Quick-add grid",
  "tag": "section",
  "class": "mf-section",
  "disabled_on": {
    "groups": ["header", "footer"]
  },
  "settings": [
    {
      "type": "header",
      "content": "Content"
    },
    {
      "type": "text",
      "id": "eyebrow",
      "label": "Eyebrow",
      "default": "Loved by 40,000+ customers"
    },
    {
      "type": "inline_richtext",
      "id": "heading",
      "label": "Heading",
      "default": "Our <em>bestsellers</em>"
    },
    {
      "type": "collection",
      "id": "collection",
      "label": "Collection"
    },
    {
      "type": "range",
      "id": "products_to_show",
      "label": "Products to show",
      "min": 2,
      "max": 12,
      "step": 1,
      "default": 4
    },
    {
      "type": "text",
      "id": "view_all_label",
      "label": "View all label",
      "default": "Shop all"
    },
    {
      "type": "header",
      "content": "Cards"
    },
    {
      "type": "select",
      "id": "card_style",
      "label": "Card style",
      "options": [
        { "value": "minimal", "label": "Minimal" },
        { "value": "elevated", "label": "Elevated" }
      ],
      "default": "elevated"
    },
    {
      "type": "select",
      "id": "image_ratio",
      "label": "Image ratio",
      "options": [
        { "value": "0.8", "label": "Portrait (4:5)" },
        { "value": "0.667", "label": "Tall (2:3)" },
        { "value": "1", "label": "Square" }
      ],
      "default": "0.8"
    },
    {
      "type": "checkbox",
      "id": "show_second_image",
      "label": "Show second image on hover",
      "default": true
    },
    {
      "type": "checkbox",
      "id": "show_rating",
      "label": "Show product rating",
      "info": "Uses the standard reviews.rating metafield written by most review apps.",
      "default": true
    },
    {
      "type": "text",
      "id": "quick_add_label",
      "label": "Quick add label",
      "default": "Quick add"
    },
    {
      "type": "text",
      "id": "added_label",
      "label": "Added confirmation",
      "default": "Added to cart"
    },
    {
      "type": "select",
      "id": "after_add",
      "label": "After adding to cart",
      "options": [
        { "value": "stay", "label": "Stay on page" },
        { "value": "cart", "label": "Go to cart" }
      ],
      "default": "stay"
    },
    {
      "type": "header",
      "content": "Layout"
    },
    {
      "type": "range",
      "id": "columns",
      "label": "Columns on desktop",
      "min": 2,
      "max": 5,
      "step": 1,
      "default": 4
    },
    {
      "type": "select",
      "id": "mobile_layout",
      "label": "Mobile layout",
      "options": [
        { "value": "grid", "label": "Two-column grid" },
        { "value": "carousel", "label": "Swipe carousel" }
      ],
      "default": "carousel"
    },
    {
      "type": "range",
      "id": "padding_top",
      "label": "Top padding",
      "min": 0,
      "max": 160,
      "step": 8,
      "unit": "px",
      "default": 72
    },
    {
      "type": "range",
      "id": "padding_bottom",
      "label": "Bottom padding",
      "min": 0,
      "max": 160,
      "step": 8,
      "unit": "px",
      "default": 72
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "color_bg",
      "label": "Background",
      "default": "#f6f3ee"
    },
    {
      "type": "color",
      "id": "color_text",
      "label": "Text",
      "default": "#17140f"
    },
    {
      "type": "color",
      "id": "color_card",
      "label": "Image background",
      "default": "#ebe5dc"
    },
    {
      "type": "color",
      "id": "color_accent",
      "label": "Accent (sale, confirmation)",
      "default": "#c2410c"
    }
  ],
  "presets": [
    {
      "name": "EX · Quick-add grid"
    }
  ]
}
{% endschema %}
Why it converts

Adding to cart from the grid removes the product-page detour: fewer clicks between browsing and buying.

Specs

File: sections/mf-quick-add-grid.liquidIn the theme editor: EX · Quick-add gridBest on: index, collection, product, page templatesSettings: 20Validated: Shopify Theme Check, no external assets

About this section

Quick-add Grid is a free Shopify Online Store 2.0 section that turns any collection into a shoppable bestseller grid. Each card shows the product's first image and swaps to the second on hover with a slow zoom, carries automatic badges (percentage saved when a compare-at price exists, Sold out, or New and Bestseller from product tags), a star rating read from the standard reviews.rating and reviews.rating_count metafields that most review apps write, the title and the price with the compare-at price struck through. A glassy Quick add bar slides up over the image: single-variant products are added in one click, and multi-variant products reveal chips for each variant so the shopper picks a size and adds it without leaving the page. Adding uses Shopify's AJAX Cart API, shows an in-button confirmation, announces it to screen readers and dispatches cart:refresh so compatible cart drawers update; if JavaScript fails, the real product form submits normally. On mobile the grid becomes a swipe carousel or a two-column grid, and quick add is always visible on touch screens. Merchants choose the collection, number of products, desktop columns, image ratio, card style, labels, padding and colors in the theme editor. The conversion principle is friction removal: every step between browsing and the cart costs buyers, and quick add collapses that path to a single tap.

Questions

Does quick add work with my theme's cart drawer?

The section adds products with Shopify's AJAX Cart API, then dispatches a cart:refresh event that many themes listen to. If your drawer does not update automatically, set After adding to cart to Go to cart, or ask Claude to hook the section into your theme's own cart refresh function.

Where do the star ratings come from?

From the reviews.rating and reviews.rating_count product metafields, the standard that review apps such as Judge.me, Okendo, Yotpo and Shopify Product Reviews write. Products without a rating simply hide the stars, and you can switch ratings off entirely.

What happens with products that have several variants?

Instead of a single button, the quick-add bar shows a chip for each variant (up to five). Clicking a chip adds that exact variant. Unavailable variants are shown struck through and disabled.

How are the badges decided?

Sold out when the product is unavailable, the percentage saved when the compare-at price is higher than the price, otherwise New or Bestseller when the product is tagged new or bestseller. Tag products in your admin to control them.

More sections