This guide explains how to install and use the Custom Subscription Widget in your Shopify theme. The widget replaces the default variant dropdown with a modern interface for selecting sizes, subscription frequency, and purchase type (Subscribe & Save or One-time).
1. Where to create variants in Shopify
- Products → All products → (open product)
- Scroll to Variants → Add options
- Add:
- Attribute option (Size/Color/Pack/etc.)
- Purchase Option with values (Deliver every 30 days, Deliver every 60 days, One-time purchase)
- Generate variants and set prices (subscription = discounted)
- Save.
2. Variant Model (must-read before adding code)
The widget looks for two kinds of variant options:
- Purchase Option (required) — identifies Subscription vs One-time
- Size/Attribute (optional) — can be Size, Color, Weight, Pack, etc.
The script auto-detects the Purchase Option by scanning variant values for these keywords:
Subscription keywords: deliver every, ships every, recurring, subscription
One-time keywords: one time, one-time, one-time purchase, single order, once
A. Single Option (basic)
- Purchase Option:
- Deliver every 30 days (subscription, discounted price e.g., 14.99)
- One-time purchase (regular price e.g., 19.99)
B. With a Second Attribute (Size/Color/etc.)
- Any attribute (Size, Color, Pack, Weight) works as the “other” option.
- The widget treats it as the selector (e.g., tabs).
- You can edit the code label “Size” → “Color/Pack/etc.” if needed.
Example:
- Option 1: Small, Medium, Large
- Option 2: Deliver every 30 days, One-time purchase
- Variants generated:
- Small × Subscription / One-time
- Medium × Subscription / One-time
- Large × Subscription / One-time
C. Multiple Subscription Frequencies
Add more subscription variants per attribute:
- Deliver every 14 days
- Deliver every 30 days
- Deliver every 60 days
- One-time purchase
3. Naming Conventions
✅ Works:
- Deliver every 30 days
- Subscription – every 2 weeks
- Ships every month
- One-time purchase
- One time / Single order
❌ Won’t work:
- Monthly (too vague)
- OT / Regular (not in keyword list)
- Sub (abbreviation not supported)
4. Install the Widget (Where to Add the Widget)
The widget should be placed inside your Product page template.
You have three approaches:
a) Custom Liquid block (preferred)
- Open Shopify Admin → Online Store → Themes → Customize
- Select the Product page template (or create a custom one, e.g., sf-product)
- Add a new Custom Liquid block inside the product form section
b) Theme code (advanced)
- Open Shopify Admin → Online Store → Themes → Edit Code
- Find your main-product.liquid (or equivalent product template)
- Insert the widget <div> and <script> inside the product form container
c) Custom Product Template
- In Shopify admin, create a new product template called sf-product
- Assign this template to products that need subscription options
- Add the widget code only inside this template so other products remain unaffected
5. Code to Add
Paste the following code into your Custom Liquid block:
{%- comment -%}
********************************************
SubscriptionFlow Subscribe Widget Code Start
********************************************
{%- endcomment -%}
<script>
// 🔹 Store all product variants (including metafields)
const productVariants = [
{% for v in product.variants %}
{
"id": {{ v.id }},
"price": {{ v.price }},
"options": {{ v.options | json }},
"save_on_each_order": {{ v.metafields.custom.save_on_each_order.value | json }}
}{% unless forloop.last %},{% endunless %}
{% endfor %}
];
</script>
<style>
/* ==========================
STYLES FOR SUBSCRIBE WIDGET
========================== */
#custom-variant-wrapper { margin:0px 0; }
.variant-tabs { display:flex; gap:10px;margin: 10px 0px 15px; }
#subscriptionBox { margin: 10px 0px 15px; }
.variant-tab { padding:8px 14px; border:1px solid #ddd; border-radius:8px; cursor:pointer; background:#fff; font-size:14px; transition:.2s; }
.variant-tab:hover { background:#f9f9f9; }
.variant-tab.active { background:#222; color:#fff; border-color:#222; }
.purchase-box, .one-time-box {
border:1px solid #e6e6e6; border-radius:12px; padding:14px; background:#fff;
display:flex; gap:18px; align-items:flex-start; cursor:pointer;
transition:border-color .2s, background .2s, box-shadow .2s; margin-bottom:12px;
}
.purchase-box.active, .one-time-box.active { border-color:#222; background:#f9f9f9; box-shadow:0 2px 8px rgba(0,0,0,.05); }
.subscribe-col { flex:1 1 60%; min-width:0; }
.freq-col { flex: 0 0 36%; min-width: 200px; align-self: center; }
.subscribe-row, .one-time-row { display:flex; gap:12px; align-items:flex-start; width:100%; }
/* Custom radio */
.custom-radio {
appearance:none; width:18px; height:18px; border:2px solid #aaa; border-radius:50%;
display:inline-block; position:relative; cursor:pointer; flex-shrink:0; margin-top:3px; transition:border .2s, background .2s;
}
.custom-radio:checked { border-color:#222; background:#222; }
.custom-radio:checked::after {
content:""; position:absolute; top:50%; left:50%; width:8px; height:8px; background:#fff; border-radius:50%;
transform:translate(-50%,-50%);
}
.subscribe-header, .one-time-header { display:flex; align-items:center; gap:8px; font-weight:700; font-size:15px; }
.price-row { display:flex; justify-content:flex-end; align-items:center; gap:8px; font-weight:700; }
.sub-old-price { text-decoration:line-through; color:#888; margin-right:6px; font-weight:600; }
.sub-new-price { font-size:1.05rem; color:#222; }
.option-benefits { margin:10px 0 6px 26px; color:#333; font-size:14px; }
.option-benefits ul { margin:0; padding-left:18px; }
.option-benefits li { margin:0; font-size:.8rem; }
.deliver-label { font-size:14px; color:#666; margin-bottom:8px; font-weight:600; }
.deliver-list { display:flex; flex-direction:column; gap:8px; }
.freq-option {
border:1px solid #d6d6d6; border-radius:8px; padding:8px 10px; text-align:left;
cursor:pointer; font-size:12px; background:#fff; transition:.2s;
}
.freq-option:hover { background:#fafafa; }
.freq-option.active { background:#222; color:#fff; border-color:#222; }
.one-time-price { font-weight:700; font-size:1rem; margin-left:auto; color:#222; }
/* Hide default Shopify picker */
[data-block-type="variant-picker"] { display:none !important; }
.product-info__block-item input { border:0; padding:0; pointer-events:none; }
#custom-variant-wrapper legend{
font-family: var(--heading-font-family);
font-weight: var(--heading-font-weight);
font-style: var(--heading-font-style);
letter-spacing: var(--heading-letter-spacing);
text-transform: var(--heading-text-transform);
overflow-wrap: anywhere;
}
@media (max-width:720px) {
.purchase-box { flex-direction:column; }
.freq-col { width:100%; order:2; }
}
</style>
<!-- Hidden frequency property for cart -->
<div id="custom-variant-wrapper"></div>
<input type="hidden" id="hidden-subscription-frequency" name="properties[_Subscription_Frequency]" value="">
<script>
document.addEventListener('DOMContentLoaded', function () {
const wrapper = document.getElementById('custom-variant-wrapper');
const hiddenInput = document.querySelector('input[name="id"]');
const selectEl = document.querySelector('select[name="id"]');
const hiddenSubInput = document.getElementById('hidden-subscription-frequency');
// 🔹 Flexible keywords to detect subscription vs one-time
const subscriptionKeywords = ["deliver every", "ships every", "recurring", "subscription"];
const oneTimeKeywords = ["one time", "one-time", "one-time purchase", "single order", "once"];
function isSubscriptionOption(label) {
const text = (label || "").toLowerCase();
return subscriptionKeywords.some(k => text.includes(k));
}
function isOneTimeOption(label) {
const text = (label || "").toLowerCase();
return oneTimeKeywords.some(k => text.includes(k));
}
// 🔹 Format prices in ZAR
function formatMoney(cents) {
return new Intl.NumberFormat('en-ZA', {
style: 'currency',
currency: 'ZAR',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(cents / 100);
}
if (!productVariants?.length) {
wrapper.innerHTML = '<div>No variants available</div>';
return;
}
/* ==========================
DETECT VARIANT OPTION INDEXES
========================== */
const optionCols = productVariants[0].options.length;
let purchaseOptionIndex = null;
for (let i = 0; i < optionCols; i++) {
for (const v of productVariants) {
const opt = (v.options[i] || "");
if (isSubscriptionOption(opt) || isOneTimeOption(opt)) {
purchaseOptionIndex = i; break;
}
}
if (purchaseOptionIndex !== null) break;
}
if (purchaseOptionIndex === null) purchaseOptionIndex = optionCols > 1 ? 1 : 0;
const sizeOptionIndex = optionCols > 1 ? (purchaseOptionIndex === 0 ? 1 : 0) : null;
const sizes = sizeOptionIndex !== null ? [...new Set(productVariants.map(v => v.options[sizeOptionIndex]).filter(Boolean))] : [];
/* ==========================
BUILD INITIAL HTML
========================== */
let html = '';
if (sizes.length > 1) {
html += '<div class="variant-picker__option-info h-stack justify-between gap-2"><div class="h-stack gap-1"><legend>Size:</legend></div></div><div class="variant-tabs" id="variantTabs">';
sizes.forEach((s, idx) => html += `<div class="variant-tab ${idx===0?'active':''}" data-size="${s}">${s}</div>`);
html += '</div>';
}
// Purchase options (Subscribe vs One-time)
html += `
<div class="variant-picker__option-info h-stack justify-between gap-2">
<div class="h-stack gap-1"><legend>Purchase Options:</legend></div>
</div>
<!-- Subscribe & Save -->
<div class="purchase-box active" id="subscriptionBox">
<div class="subscribe-col">
<div class="subscribe-row">
<input class="custom-radio" type="radio" name="purchase_choice" id="subscribeRadio" value="subscription" checked>
<div>
<div class="subscribe-header"><span>Subscribe & Save</span></div>
<div style="margin-top:6px;">
<span class="sub-old-price" id="subOldPrice"></span>
<span class="sub-new-price" id="subNewPrice"></span>
</div>
</div>
</div>
</div>
<div class="freq-col">
<div class="deliver-label">Deliver every</div>
<div class="deliver-list" id="deliverList"></div>
</div>
</div>
<!-- One-time Purchase -->
<div class="one-time-box" id="oneTimeBox">
<div class="one-time-row">
<input class="custom-radio" type="radio" name="purchase_choice" id="oneTimeRadio" value="onetime">
<div class="one-time-header">One-time</div>
</div>
<div class="one-time-price" id="oneTimePrice"></div>
</div>
`;
wrapper.innerHTML = html;
/* ==========================
DOM ELEMENT REFERENCES
========================== */
const deliverList = document.getElementById('deliverList');
const subOldPriceEl = document.getElementById('subOldPrice');
const subNewPriceEl = document.getElementById('subNewPrice');
const oneTimePriceEl = document.getElementById('oneTimePrice');
const subscribeRadio = document.getElementById('subscribeRadio');
const oneTimeRadio = document.getElementById('oneTimeRadio');
const subscriptionBox = document.getElementById('subscriptionBox');
const oneTimeBox = document.getElementById('oneTimeBox');
let currentSize = sizes.length > 0 ? sizes[0] : null;
let activeSubscriptionVariantId = null;
let activeOneTimeVariantId = null;
/* ==========================
VARIANT HELPERS
========================== */
function subscriptionVariantsForSize(size) {
return productVariants.filter(v => {
const purchaseVal = (v.options[purchaseOptionIndex]||'');
const sizeMatch = sizeOptionIndex===null || v.options[sizeOptionIndex]===size;
return sizeMatch && isSubscriptionOption(purchaseVal);
});
}
function oneTimeVariantForSize(size) {
return productVariants.find(v => {
const purchaseVal = (v.options[purchaseOptionIndex]||'');
const sizeMatch = sizeOptionIndex===null || v.options[sizeOptionIndex]===size;
return sizeMatch && isOneTimeOption(purchaseVal);
});
}
function setSelectedVariantId(id) {
if (!id) return;
if (hiddenInput) hiddenInput.value = id;
if (selectEl) selectEl.value = id;
(hiddenInput||selectEl)?.dispatchEvent(new Event('change',{bubbles:true}));
}
/* ==========================
RENDER FUNCTION
========================== */
function renderForSize(size) {
const subs = subscriptionVariantsForSize(size);
const oneTime = oneTimeVariantForSize(size);
deliverList.innerHTML = '';
activeSubscriptionVariantId = null;
// Subscription options
if (subs.length) {
subs.forEach((v,idx) => {
const btn=document.createElement('button');
btn.type='button'; btn.className='freq-option';
btn.textContent=v.options[purchaseOptionIndex];
btn.dataset.variantId=v.id;
btn.addEventListener('click',()=> {
document.querySelectorAll('.freq-option').forEach(el=>el.classList.remove('active'));
btn.classList.add('active');
setSelectedVariantId(v.id); activeSubscriptionVariantId=v.id;
subOldPriceEl.textContent = oneTime?formatMoney(oneTime.price):'';
subNewPriceEl.textContent = formatMoney(v.price);
hiddenSubInput.value = "Subscription"; // ✅ set property
subscribeRadio.checked=true;
subscriptionBox.classList.add('active'); oneTimeBox.classList.remove('active');
});
deliverList.appendChild(btn);
if(idx===0){
btn.classList.add('active');
activeSubscriptionVariantId=v.id;
subOldPriceEl.textContent=oneTime?formatMoney(oneTime.price):'';
subNewPriceEl.textContent=formatMoney(v.price);
if (subscribeRadio.checked) hiddenSubInput.value = "Subscription";
}
});
if(activeSubscriptionVariantId) setSelectedVariantId(activeSubscriptionVariantId);
} else {
deliverList.innerHTML='<div style="color:#666;font-size:13px;">No subscription options</div>';
subOldPriceEl.textContent=''; subNewPriceEl.textContent='';
hiddenSubInput.value = '';
}
// One-time option
if (oneTime) {
oneTimeBox.style.display='flex';
oneTimePriceEl.textContent=formatMoney(oneTime.price);
activeOneTimeVariantId=oneTime.id;
if (oneTimeRadio.checked) {
hiddenSubInput.value = "";
setSelectedVariantId(activeOneTimeVariantId);
}
} else {
oneTimeBox.style.display='none'; activeOneTimeVariantId=null;
}
}
/* ==========================
TAB HANDLING
========================== */
if (sizes.length > 1) {
document.querySelectorAll('#variantTabs .variant-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('#variantTabs .variant-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentSize = tab.dataset.size;
renderForSize(currentSize);
});
});
}
/* ==========================
PURCHASE BOX HANDLING
========================== */
subscriptionBox.addEventListener('click',()=> {
if(activeSubscriptionVariantId){
subscribeRadio.checked=true;
hiddenSubInput.value = "Subscription";
subscriptionBox.classList.add('active'); oneTimeBox.classList.remove('active');
const activeBtn=document.querySelector('.freq-option.active')||document.querySelector('.freq-option');
activeBtn?.click();
}
});
oneTimeBox.addEventListener('click',()=> {
if(activeOneTimeVariantId){
oneTimeRadio.checked=true;
hiddenSubInput.value = "";
subscriptionBox.classList.remove('active'); oneTimeBox.classList.add('active');
setSelectedVariantId(activeOneTimeVariantId);
}
});
/* ==========================
INITIAL RENDER
========================== */
renderForSize(currentSize);
});
</script>
{%- comment -%}
********************************************
SubscriptionFlow Subscribe Widget Code End
********************************************
{%- endcomment -%}
6. How It Works
Size Tabs → Customers switch between product sizes.
Purchase Options → Two boxes:
- Subscribe & Save (shows discount price, and frequency options).
- One-Time Purchase (shows regular price).
Hidden Inputs → Script updates Shopify’s hidden <input name="id"> so the correct variant ID is added to cart.
Checkout Integration → properties [Subscription Frequency] field captures whether the customer chose subscription or one-time.
Final Output :
7. “Buy it now” (SubscriptionFlow) via Custom Liquid only
SF hosted Buy Now Button when a subscription variant is selected and falls back to Shopify’s native button for one-time variants.
Steps:
1. Shopify Admin → Online Store → Themes → Customize
2. Open your Product template
3. Add block → Custom Liquid inside/near the product form
4. Paste the following code and Save
Custom Liquid code (paste as-is) :
<script>
document.addEventListener("DOMContentLoaded", function () {
const product = {{ product | json }};
const sfBaseUrl = "https://example.subscriptionflow.com/en/hosted-page/commerceflow";
const shopifyButtons = document.querySelectorAll(".shopify-payment-button__button, .product-form__submit, .product_submit_button");
if (shopifyButtons.length === 0) return;
// Create a map to store corresponding SF links
const sfLinks = [];
// Create and insert an SF <a> next to each Shopify Buy Now button
shopifyButtons.forEach((btn, index) => {
const sfLink = document.createElement("a");
sfLink.textContent = "Buy it now";
sfLink.className = btn.className + " sf-checkout-link";
sfLink.id = "sf-btn-" + index;
sfLink.style.display = "none";
sfLink.setAttribute("target", "_blank");
sfLink.setAttribute("rel", "noopener noreferrer");
btn.parentNode.insertBefore(sfLink, btn.nextSibling);
sfLinks.push({ original: btn, sf: sfLink });
});
const isSubscription = (title) => {
const lower = title.toLowerCase();
return lower.includes("monthly") || lower.includes("every");
};
const getQuantity = () => {
const qtyInput = document.querySelector(".m-quantity__input, .quantity__input");
const qty = parseInt(qtyInput?.value);
return isNaN(qty) || qty < 1 ? 1 : qty;
};
const getVariantId = () => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('variant') || '{{ product.selected_or_first_available_variant.id }}';
};
function updateBuyButtons() {
const variantId = getVariantId();
const selectedVariant = product.variants.find(v => v.id == variantId);
if (!selectedVariant) return;
const isSub = isSubscription(selectedVariant.title);
const q = getQuantity();
const sfUrl = `${sfBaseUrl}?items[0][pr]=${product.id}&items[0][pl]=${selectedVariant.id}&items[0][q]=${q}&cart={{ shop.url }}/cart`;
sfLinks.forEach(({ original, sf }) => {
if (isSub) {
sf.href = sfUrl;
sf.style.display = "inline-block";
original.style.display = "none";
} else {
sf.style.display = "none";
original.style.display = "inline-block";
}
});
}
// Run on page load
updateBuyButtons();
// Update on variant or quantity change
window.addEventListener("change", updateBuyButtons);
const qtyInput = document.querySelector(".m-quantity__input, .quantity__input");
if (qtyInput) {
qtyInput.addEventListener("input", updateBuyButtons);
}
});
</script>
Notes
• sfBaseUrl: replace with your store’s SF domain.
• Selectors: if your theme uses different CTA classes, add them to
.shopify-payment-button__button, .product-form__submit, .product_submit_button.
• Same-tab checkout: change target="_blank" → target="_self" if preferred.
• Detection logic: current detection uses variant title keywords: “monthly”, “every”.
If your naming differs (e.g., “Deliver every 30 days”, “Ships every month”), extend the function.
Quick QA
• Subscription variant selected → SF Buy it now appears; Shopify CTA hides.
• One-time variant selected → Shopify CTA appears; SF link hides.
• Changing quantity updates the SF URL.
• Changing variant updates the SF URL.
Comments
0 comments
Article is closed for comments.