Offering shopify product samples is a proven way to build trust, improve conversions, and reduce returns. In this guide, we’ll walk through how to add a “Get a Sample” button to products that triggers a separate checkout flow just for samples — all without using third-party apps.
What This Guide Covers
By the end of this tutorial, your Shopify store will support:
- “Get a Sample” button on eligible products
- A modal popup that displays selected sample items
- Dedicated sample-only checkout (your main cart stays untouched)
- Cart auto-clears when returning from Shopify sample checkout
- Support for a sample limit (e.g. 5 items max)
- No apps required — built with custom JS + Liquid
Requirements
- Shopify store using an Online Store 2.0 theme (e.g., Dawn).
- Access to theme code (via Admin > Online Store > Themes > Edit Code).

1. Create a Snippet : snippets/GetASample.liquid
Paste the following code into a new snippet:
<!-- Snippet/GetASample.liquid -->
<style>
.btn--get-sample {
--bg: var(--button-primary-background, #1c1c1c);
--fg: var(--button-primary-text, #ffffff);
display: block;
width: 100%;
padding: 13px;
border: 1px solid var(--bg);
background: transparent;
color: var(--bg);
font-size: 15px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
border-radius: 4px;
cursor: pointer;
text-align: center;
text-decoration: none;
transition: all 0.25s ease;
}
.btn--get-sample:hover,
.btn--get-sample:focus {
background: var(--bg);
color: var(--fg);
}
/* Modal Container */
#sample-modal {
position: fixed;
left: 0;
right: 0;
bottom: 0;
display: none;
z-index: 9999;
background: #f6f6f6;
border-top: 1px solid #ccc;
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.1);
transform: translateY(100%);
opacity: 0;
transition: transform 0.35s ease-out, opacity 0.35s ease-out;
max-height: 90vh;
overflow-y: auto;
}
#sample-modal.active {
display: flex;
flex-direction: column;
transform: translateY(0%);
opacity: 1;
}
.sample-modal-inner {
width: 100%;
padding: 20px;
position: relative;
}
/* Close Button */
.sample-close {
position: absolute;
top: 10px;
right: 15px;
background: transparent;
border: none;
font-size: 24px;
cursor: pointer;
color: #555;
}
/* Header */
.sample-header {
margin-bottom: 20px;
}
.sample-header h3 {
font-size: 18px;
margin: 0 0 5px;
}
.sample-header p {
font-size: 14px;
color: #333;
margin: 0;
}
/* Sample List */
.sample-list {
display: flex;
gap: 20px;
padding: 0 20px 20px;
overflow-x: auto;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE 10+ */
}
.sample-list::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
/* Sample Cards */
.sample-card {
flex: 0 0 auto;
width: 120px;
text-align: center;
position: relative;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
padding: 10px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.sample-card img {
width: 100%;
height: 80px;
object-fit: cover;
border-radius: 4px;
}
.sample-card-title {
margin-top: 8px;
font-size: 13px;
color: #333;
line-height: 1.4;
}
.sample-card-remove {
position: absolute;
top: 6px;
right: 6px;
border: none;
background: transparent;
font-size: 16px;
cursor: pointer;
color: #999;
}
.sample-card-remove:hover {
color: #000;
}
/* Footer */
.sample-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-top: 1px solid #ccc;
background: #f6f6f6;
}
.sample-clear {
background: transparent;
border: none;
font-size: 14px;
text-decoration: underline;
cursor: pointer;
color: #333;
}
.sample-checkout-btn {
background: #000;
color: #fff;
padding: 12px 24px;
border: none;
border-radius: 4px;
text-decoration: none;
font-weight: 600;
font-size: 14px;
transition: background 0.3s ease;
}
.sample-checkout-btn:hover {
background: #333;
}
.custom-loader-overlay {
position: fixed;
inset: 0;
background: rgba(255, 255, 255, 0.85);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}
.custom-loader-spinner {
border: 4px solid #ccc;
border-top: 4px solid #000;
border-radius: 50%;
width: 36px;
height: 36px;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
{%- assign sample_product = product.metafields.custom.product.value -%}
{%- assign sample_image_url = sample_product.images.first | json -%}
<a class="btn--get-sample"
id="GetSample-{{ sample_product.id }}"
href="#"
data-sample-variant="{{ sample_product.first_available_variant.id }}"
data-original-product="{{ product.id }}"
data-product-title="{{ sample_product.title | escape }}"
data-sample-image-url={{ sample_image_url }}>
Get a Sample
</a>
Style and modal markup are also included in the same snippet or embedded via separate assets.
2. Add Modal Markup to footer.liquid
Place this at the bottom of your theme:
<div id="sample-modal" class="sample-modal" style="display: none;">
<div class="sample-modal-inner">
<button class="sample-close" onclick="closeSampleModal()">×</button>
<div class="sample-header">
<h3>SAMPLES</h3>
<p><strong>Feel the Quality Before You Commit</strong><br>
See, touch, and feel the perfect tiles for your project.</p>
</div>
<div id="sample-list" class="sample-list"></div>
<div class="sample-footer">
<button class="sample-clear">Clear All</button>
<a href="#" class="sample-checkout-btn">Checkout</a>
</div>
</div>
</div>

3. Create JavaScript File : assets/sample_product_btn.js
Go to the Assets folder, choose 'Create a blank file', then enter the file name and extension.

Upload a new JS asset with the following script, or paste your custom version if already configured:
(function () {
/* ---------- CONSTANTS ---------- */
const LS_SAMPLES = 'sample_products'; // stores current sample list
const MAX_SAMPLES = 5;
const CHECKOUT_KEY = 'in_sample_checkout'; // flag to detect return from checkout
/* ---------- LOCAL STORAGE HELPERS ---------- */
const getSamples = () => JSON.parse(localStorage.getItem(LS_SAMPLES) || '[]');
const saveSamples = (arr) => localStorage.setItem(LS_SAMPLES, JSON.stringify(arr));
/* ---------- UTILITY ---------- */
const normalizeURL = src =>
!src ? '' : src.startsWith('http') ? src : src.startsWith('//') ? 'https:' + src : src;
/* ---------- CART COUNT BUBBLE ---------- */
const updateCartCountBubble = () => {
fetch('/cart.js')
.then(r => r.json())
.then(cart => {
const bubble = document.querySelector('.cart-count-bubble');
if (!bubble) return;
const total = cart.items.reduce((sum, i) => sum + i.quantity, 0);
bubble.innerHTML = `
<span aria-hidden="true">${total}</span>
<span class="visually-hidden">${total} ${total === 1 ? 'item' : 'items'}</span>`;
})
.catch(console.error);
};
/* ---------- MODAL OPEN/CLOSE ---------- */
const openModal = () => {
const m = document.getElementById('sample-modal');
if (m) { m.style.display = 'flex'; requestAnimationFrame(() => m.classList.add('active')); }
};
const closeModal = () => {
const m = document.getElementById('sample-modal');
if (m) { m.classList.remove('active'); setTimeout(() => m.style.display = 'none', 350); }
};
window.closeSampleModal = closeModal;
/* ---------- RENDER SAMPLE LIST ---------- */
const renderSampleList = () => {
const list = document.getElementById('sample-list');
const go = document.querySelector('.sample-checkout-btn');
if (!list || !go) return;
const samples = getSamples();
list.innerHTML = '';
if (!samples.length) { go.href = '#'; go.classList.add('disabled'); return; }
go.classList.remove('disabled');
samples.forEach(s =>
list.insertAdjacentHTML('beforeend', `
<div class="sample-card">
<button class="sample-card-remove" data-v="${s.variantId}" aria-label="Remove ${s.title}">×</button>
<img loading="lazy" src="${s.image}" alt="${s.title}">
<div class="sample-card-title">${s.title}</div>
</div>`));
go.href = `/cart/${samples.map(s => `${s.variantId}:1`).join(',')}`;
};
/* ---------- “GET SAMPLE” BUTTON CLICK ---------- */
document.addEventListener('click', e => {
const btn = e.target.closest('.btn--get-sample');
if (!btn) return;
e.preventDefault();
const variantId = btn.dataset.sampleVariant?.trim();
if (!variantId) {
console.warn(' No sample variant on button – click ignored', btn);
return;
}
let fullProd = Number((btn.dataset.originalProduct || '').split('/Product/').pop() || 0);
/* -- Remove any full‑size product in cart -- */
if (fullProd) {
fetch('/cart.js')
.then(r => r.json())
.then(cart => Promise.all(
cart.items
.filter(i => i.product_id === fullProd)
.map(i => fetch('/cart/change.js', {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify({ id: i.key, quantity: 0 })
}))
).then(updateCartCountBubble));
}
/* -- Manage samples list -- */
let samples = getSamples();
if (samples.some(s => s.variantId === variantId)) { openModal(); renderSampleList(); return; }
if (samples.length >= MAX_SAMPLES) { alert(`Max ${MAX_SAMPLES} samples.`); openModal(); renderSampleList(); return; }
samples.push({
variantId,
title : btn.dataset.productTitle,
image : normalizeURL(btn.dataset.sampleImageUrl)
});
saveSamples(samples);
openModal(); renderSampleList();
});
/* ---------- REMOVE/CLEAR IN MODAL ---------- */
document.addEventListener('click', e => {
const rm = e.target.closest('.sample-card-remove');
if (rm) { saveSamples(getSamples().filter(s => s.variantId !== rm.dataset.v)); renderSampleList(); return; }
if (e.target.closest('.sample-clear')) { localStorage.removeItem(LS_SAMPLES); renderSampleList(); }
});
/* ---------- CHECKOUT WITH SAMPLES ---------- */
document.addEventListener('click', async e => {
const go = e.target.closest('.sample-checkout-btn');
if (!go) return;
e.preventDefault();
if (go.dataset.locked) return;
go.dataset.locked = 'true'; go.classList.add('loading');
const samples = getSamples();
if (!samples.length) { alert('No samples selected.'); go.dataset.locked = ''; go.classList.remove('loading'); return; }
localStorage.setItem(CHECKOUT_KEY, 'true');
await fetch('/cart/clear.js', { method: 'POST' });
const ids = samples.map(s => `${s.variantId}:1`).join(',');
window.location.href = `/cart/${ids}?checkout`;
});
/* ---------- BACK‑BUTTON HANDLING ---------- */
window.addEventListener('pageshow', ev => {
const cameBack = ev.persisted || window.performance?.navigation?.type === 2;
if (!cameBack) return;
const fromCheckout = localStorage.getItem(CHECKOUT_KEY) === 'true';
if (fromCheckout) {
console.log('↩️ Returned from sample checkout – clearing cart again');
localStorage.removeItem(CHECKOUT_KEY);
fetch('/cart/clear.js', { method: 'POST' })
.finally(() => setTimeout(updateCartCountBubble, 300));
} else {
setTimeout(updateCartCountBubble, 100);
}
});
/* ---------- INITIAL BUBBLE REFRESH ---------- */
document.addEventListener('DOMContentLoaded', updateCartCountBubble);
})();
4. Load Script in theme.liquid
Include the JS file before the closing </body> tag:

5. Update main-product.liquid Section
Ensure the <product-info> element has the proper attributes to support sample tracking:
<product-info
id="MainProduct-{{ section.id }}"
class="section-{{ section.id }}-padding gradient color-{{ section.settings.color_scheme }}"
data-section="{{ section.id }}"
data-product-id="{{ product.id }}"
data-update-url="true"
data-url="{{ product.url }}"
{% if section.settings.image_zoom == 'hover' %}
data-zoom-on-hover
{% endif %}
{% assign sample_item = cart.items | where: "product_id", product.id | where: "properties._sample", "true" | first %}
{% if sample_item %}
data-sample-variant="{{ sample_item.variant_id }}"
{% endif %}
>

6. Add Sample Button Render Code in buy-buttons.liquid
Find the line with:
And replace it with:
{% if product.metafields.custom.product != blank %}
{% render 'GetASample' %}
{% endif %}

The “Get a Sample” button only appears on products where a linked sample product is added via the metafield custom.product. This ensures the button is conditionally visible only on eligible products.

When the “Get a Sample” button is clicked, a modal opens at the bottom of the screen showing the selected sample products. Customers can review, remove, or proceed to checkout directly from this modal

When the Checkout button is clicked in the sample modal, only the selected sample products are added to the cart and the user is redirected directly to the Shopify checkout page. This ensures a smooth, sample-only checkout flow. The main cart remains untouched.

Summary
We now have a complete sample product selection flow:
- Only sample variants can be selected
- Full-size version is removed automatically
- Max 5 samples per session
- Cart is cleared on Shopify sample checkout
- Cart is not restored after Shopify sample checkout
- Script and modal integrated cleanly with your theme
Conclusion
In this implementation, we added a “Get a Sample” button that lets customers add up to 5 sample products. When the Checkout button is clicked, the cart is fully cleared and only the selected sample variants are sent to checkout. The original cart is not restored after returning, keeping the sample flow completely separate. We also configured <product-info> to support sample tracking, and included the required script and modal setup to provide a smooth, isolated Shopify sample checkout experience.