Subscriptions (subscribe and save)

Hi, is it possible to make custom button like the picture below? When we click subscribe and save, it will show the purchase option for subscribe and save (delivery frequency and so on). Then we click one-time purchase, it will show our normal purchase details from shopify. Is it possible?

I already surveying few apps but most of them didnt offer the design like this

Hey @veluxe1 ,

Yes, this should be possible with custom code, and the Subscribe & Save and One time Purchase options can be combined into a single purchase selector like the design in your screenshot.

Before I provide the code, could you share your storefront URL and, if the store is password protected, the storefront password as well?

I’d like to check your current theme and subscription setup first, because the exact implementation will depend on how your product form and subscription selling plans are currently configured.

Once I can see the setup, I can provide code that combines both options properly so selecting Subscribe & Save displays the subscription details such as delivery frequency, while selecting One time Purchase switches back to the normal Shopify purchase options.

If you found this answer helpful, you can mark it as the solution.

Thank You !

this design is possible, but it requires either checkout extensions (Shopify Plus only) or custom development. That’s why most standard apps can’t deliver it their UI is locked to their built-in patterns.

What plan are you on? That’s the real constraint here.

pass : rteobi

Horizon theme and i just add the custom liquid button (idk what to do after that)

Thanks! I can see that you’re using the Horizon theme, and adding a Custom Liquid block is a good starting point.

Before I give you the code, could you tell me which subscription app you’re using (or send me the app link)?

The reason I’m asking is that the Subscribe & Save options are handled through the app’s Shopify selling plans, so I want to make sure the code connects to your existing subscription setup correctly rather than giving you generic code that may not work with your app.

Once you send me the subscription app name, I can give you the exact code to add to the Custom Liquid block so the two options work together:

Subscribe & Save → shows the available delivery frequency/subscription options.

One time Purchase → switches back to the normal Shopify purchase option.

Then we can style it to match the design you originally showed.

This App

It says the code has errors

{% if product.selling_plan_groups.size > 0 %}

<div class="custom-purchase-options" id="custom-purchase-options-{{ section.id }}">

<div class="purchase-option purchase-option--active" data-type="subscription">
  <label>
    <input type="radio" name="purchase-type-{{ section.id }}" value="subscription" checked>
    <span class="purchase-option__content">
      <strong>Subscribe &amp; Save</strong>
      <small>Save on every delivery</small>
    </span>
  </label>
</div>

<div class="subscription-plans">
  {% for group in product.selling_plan_groups %}
    {% for plan in group.selling_plans %}
      <label class="subscription-plan">
        <input
          type="radio"
          name="selling-plan-{{ section.id }}"
          value="{{ plan.id }}"
          {% if forloop.first %}checked{% endif %}
        >
        <span>{{ plan.name }}</span>
      </label>
    {% endfor %}
  {% endfor %}
</div>

<div class="purchase-option" data-type="one-time">
  <label>
    <input type="radio" name="purchase-type-{{ section.id }}" value="one-time">
    <span class="purchase-option__content">
      <strong>One-time Purchase</strong>
      <small>Buy once at the regular price</small>
    </span>
  </label>
</div>
#custom-purchase-options-{{ section.id }} { margin: 20px 0; } #custom-purchase-options-{{ section.id }} .purchase-option { border: 1px solid rgba(0,0,0,.2); border-radius: 8px; padding: 15px; margin-bottom: 10px; cursor: pointer; } #custom-purchase-options-{{ section.id }} .purchase-option--active { border-color: #111; background: #fafafa; } #custom-purchase-options-{{ section.id }} label { display: flex; align-items: center; gap: 10px; cursor: pointer; } #custom-purchase-options-{{ section.id }} .purchase-option__content { display: flex; flex-direction: column; gap: 4px; } #custom-purchase-options-{{ section.id }} small { opacity: .65; } #custom-purchase-options-{{ section.id }} .subscription-plans { padding: 0 15px 15px 40px; } #custom-purchase-options-{{ section.id }} .subscription-plan { margin: 8px 0; font-size: 14px; }

<script
(function() {
const container = document.getElementById(
'custom-purchase-options-{{ section.id }}'
);
if (!container) return;
const subscriptionOption = container.querySelector(
'[data-type="subscription"]'
);
const oneTimeOption = container.querySelector(
'[data-type="one-time"]'
);
const subscriptionPlans = container.querySelector(
'.subscription-plans'
);
function getProductForm() {
return document.querySelector('form[action*="/cart/add"]');
}
function updateSellingPlan(planId) {
const form = getProductForm();
if (!form) return;
let input = form.querySelector(
'input[name="selling_plan"]'
);
if (!input) {
input = document.createElement('input');
input.type = 'hidden';
input.name = 'selling_plan';
form.appendChild(input);
}
input.value = planId || '';
}
function selectSubscription() {
subscriptionOption.classList.add('purchase-option--active');
oneTimeOption.classList.remove('purchase-option--active');
subscriptionPlans.style.display = 'block';
const selectedPlan = container.querySelector(
'input[name="selling-plan-{{ section.id }}"]:checked'
);
updateSellingPlan(
selectedPlan ? selectedPlan.value : ''
);
}
function selectOneTime() {
oneTimeOption.classList.add('purchase-option--active');
subscriptionOption.classList.remove('purchase-option--active');
subscriptionPlans.style.display = 'none';
updateSellingPlan('');
}
subscriptionOption.addEventListener('click', selectSubscription);
oneTimeOption.addEventListener('click', selectOneTime);
container
.querySelectorAll('input[name="selling-plan-{{ section.id }}"]')
.forEach(function(input) {
input.addEventListener('change', function() {
updateSellingPlan(this.value);
});
});
selectSubscription();
})();
dif %}

HI @veluxe1

Yes, this is possible. You can build the two options as a custom purchase selector, with Subscriber & Save revealing the subscription frequency/options and One-time Purchase revealing the normal Shopify purchase flow.

The important part is that the subscription option needs to be connected to a subscription app/payment integration that supports Shopify selling plans. The visual design itself can generally be handled with theme code/CSS, even if the subscription app doesn’t provide this exact layout out of the box.

The setup would typically be:

  1. Subscribe & Save → selected by default or when clicked → show delivery frequency and subscription price.
  2. One-time Purchase → hide the subscription fields → show the standard Shopify price/purchase option.
  3. Both options use the same product/variant, but subscription selection adds the appropriate selling plan to the cart.
  4. The styling can then be matched to the design in your screenshot with custom Liquid/CSS/JavaScript.

So you don’t necessarily need an app that offers that exact visual design. What matters more is that the subscription app supports Shopify selling plans and allows the purchase widget to be customised or integrated into the theme.

If you’re comfortable editing the theme, this is quite achievable without rebuilding the entire product page.

Can you help me? I already make the custom button

Hi again!

You do not need Shopify Plus.

Tested on a Horizon store with a live subscription app u mentioned above.

Result:

Where it goes

  • Online Store > Themes > Customize, open a product page
  • In the Custom Liquid block you already added (keep it in the product area, above Add to cart), paste the code below, then Save

The code

{%- assign cw_product = closest.product | default: product -%}
{%- if cw_product.selling_plan_groups.size > 0 -%}
  {%- assign cw_group = cw_product.selling_plan_groups | where: 'name', 'Subscribe & Save' | first -%}
  {%- unless cw_group -%}{%- assign cw_group = cw_product.selling_plan_groups.first -%}{%- endunless -%}
  {%- assign cw_uid = block.id | default: section.id -%}

  <style>
    .cw-po{--cw-accent:#2f5d54;--cw-soft:#f3efe9;margin:0 0 1.1rem;font-family:inherit}
    .cw-po__seg{display:flex;border-radius:12px;overflow:hidden;border:1px solid rgba(0,0,0,.08)}
    .cw-po__btn{flex:1;padding:.85rem 1rem;border:0;background:var(--cw-soft);cursor:pointer;font:inherit;font-weight:600;color:#2a2a2a;line-height:1.25;text-align:center;transition:background .15s,color .15s}
    .cw-po__btn small{display:block;font-weight:500;font-size:.78em;opacity:.8;margin-top:.1rem}
    .cw-po__btn.is-active{background:var(--cw-accent);color:#fff}
    .cw-po__body{margin-top:.85rem}
    .cw-po__price{font-size:1.6rem;font-weight:700;color:var(--cw-accent);line-height:1.1}
    .cw-po__note{font-size:.9rem;color:#555;margin-top:.2rem}
    .cw-po__freq{margin-top:.7rem}
    .cw-po__freq label{display:block;font-size:.8rem;color:#555;margin-bottom:.3rem}
    .cw-po__freq select{width:100%;padding:.6rem .7rem;border:1px solid rgba(0,0,0,.22);border-radius:8px;font:inherit;background:#fff}
    .cw-po__freq[hidden]{display:none}
  </style>

  <div class="cw-po" data-cw-po data-section="{{ cw_uid }}">
    <div class="cw-po__seg">
      <button type="button" class="cw-po__btn is-active" data-mode="subscription">
        Subscribe and Save<small data-cw-save></small>
      </button>
      <button type="button" class="cw-po__btn" data-mode="onetime">
        One time purchase<small>Standard price</small>
      </button>
    </div>
    <div class="cw-po__body">
      <div class="cw-po__price" data-cw-price></div>
      <div class="cw-po__note" data-cw-note></div>
      <div class="cw-po__freq" data-cw-freq>
        <label for="cw-freq-{{ cw_uid }}">Delivery frequency</label>
        <select id="cw-freq-{{ cw_uid }}" data-cw-plan>
          {%- for plan in cw_group.selling_plans -%}
            <option value="{{ plan.id }}">{{ plan.name }}</option>
          {%- endfor -%}
        </select>
      </div>
    </div>
  </div>

  <script>
    (function(){
      var DATA = {
        {%- for variant in cw_product.variants -%}
        "{{ variant.id }}": {
          oneTime: {{ variant.price | json }},
          oneTimeStr: {{ variant.price | money | json }},
          plans: {
            {%- for alloc in variant.selling_plan_allocations -%}
            "{{ alloc.selling_plan.id }}": { price: {{ alloc.price | json }}, priceStr: {{ alloc.price | money | json }}, name: {{ alloc.selling_plan.name | json }} }{% unless forloop.last %},{% endunless %}
            {%- endfor -%}
          }
        }{% unless forloop.last %},{% endunless %}
        {%- endfor -%}
      };
      var SECTION = {{ cw_uid | json }};

      function init(){
        var root = document.querySelector('[data-cw-po][data-section="'+SECTION+'"]');
        if(!root || root.dataset.cwInit) return;
        var form = (root.closest('form[data-type="add-to-cart-form"]'))
          || (root.closest('.shopify-section, product-information, .product, [class*="product"]') || document).querySelector('form[data-type="add-to-cart-form"]')
          || document.querySelector('form[data-type="add-to-cart-form"]');
        if(!form) return;
        root.dataset.cwInit = "1";

        var variantInput = form.querySelector('input[name="id"]');
        var planSelect = root.querySelector('[data-cw-plan]');
        var priceEl = root.querySelector('[data-cw-price]');
        var noteEl = root.querySelector('[data-cw-note]');
        var freqEl = root.querySelector('[data-cw-freq]');
        var saveEl = root.querySelector('[data-cw-save]');
        var btns = root.querySelectorAll('.cw-po__btn');
        var mode = 'subscription';

        function vData(){
          var keys = Object.keys(DATA);
          var vid = variantInput && DATA[variantInput.value] ? variantInput.value : keys[0];
          return DATA[vid] || {};
        }
        function planInput(create){
          var i = form.querySelector('input[name="selling_plan"][data-cw]');
          if(!i && create){ i=document.createElement('input'); i.type='hidden'; i.name='selling_plan'; i.setAttribute('data-cw',''); form.appendChild(i); }
          return i;
        }
        function saveText(vd, id){
          var p=vd.plans&&vd.plans[id]; if(!p||!vd.oneTime) return '';
          var pct=Math.round((1-(p.price/vd.oneTime))*1000)/10;
          return pct>0?(' · save '+pct+'%'):'';
        }
        function render(){
          var vd=vData();
          var id=planSelect?planSelect.value:(vd.plans?Object.keys(vd.plans)[0]:'');
          btns.forEach(function(b){var on=b.dataset.mode===mode;b.classList.toggle('is-active',on);});
          if(mode==='subscription'){
            freqEl.hidden=false;
            var p=vd.plans&&vd.plans[id];
            priceEl.textContent=p?p.priceStr:(vd.oneTimeStr||'');
            var freqText=planSelect&&planSelect.selectedIndex>-1?planSelect.options[planSelect.selectedIndex].textContent:'';
            if(freqText.indexOf(',')>-1) freqText=freqText.slice(freqText.lastIndexOf(',')+1).trim();
            noteEl.textContent=freqText;
            if(saveEl) saveEl.textContent=saveText(vd,id);
            var inp=planInput(true); if(inp) inp.value=id;
          } else {
            freqEl.hidden=true;
            priceEl.textContent=vd.oneTimeStr||'';
            noteEl.textContent='One-time purchase';
            var ex=planInput(false); if(ex) ex.remove();
          }
        }
        btns.forEach(function(b){b.addEventListener('click',function(){mode=b.dataset.mode;render();});});
        if(planSelect) planSelect.addEventListener('change',render);
        form.addEventListener('change',function(){setTimeout(render,60);});
        render();
      }
      if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init);
      else init();
      document.addEventListener('shopify:section:load',init);
    })();
  </script>
{%- endif -%}

Notes:

  • It reads your subscription app’s plans automatically. Whatever delivery options you set up (weekly, monthly, and so on) appear in the frequency dropdown.
  • Subscribe shows the discounted price and the frequency picker. One time shows the normal price and hides the picker.
  • Change the accent colour on the first CSS line (--cw-accent).
  • Horizon still shows its own price above the widget. Leave it, or hide the theme price block on the product if you want only the widget’s price.

Regards,
Ajay,
Try Pinflow - Pinterest Virtual Assistant

Great it works so far, how to make the button selector more clean and not too big?

And where the exact position if i want to add installment html only for one time purchase (i already have the html)

They live in the same Custom Liquid block.

Replace the block’s code with the version below.

Now

  1. the selector is now two slim pills instead of tall blocks,
  2. it has a marked spot for your installment HTML that only appears on One time.

Add your installment HTML

  • In the code, find these two lines near the bottom of the block:
    <!-- INSTALLMENT HTML START --> and <!-- INSTALLMENT HTML END -->
  • Paste your installment HTML between them
  • It shows only when One time is selected, and hides on Subscribe. The delivery frequency hides at the same time.

The code

{%- assign cw_product = closest.product | default: product -%}
{%- if cw_product.selling_plan_groups.size > 0 -%}
  {%- assign cw_group = cw_product.selling_plan_groups | where: 'name', 'Subscribe & Save' | first -%}
  {%- unless cw_group -%}{%- assign cw_group = cw_product.selling_plan_groups.first -%}{%- endunless -%}
  {%- assign cw_uid = block.id | default: section.id -%}

  <style>
    .cw-po{--cw-accent:#2f5d54;--cw-line:rgba(0,0,0,.14);margin:0 0 1rem;font-family:inherit}
    .cw-po__seg{display:flex;gap:.5rem}
    .cw-po__btn{flex:1;display:flex;align-items:center;justify-content:center;gap:.4rem;
      padding:.55rem .6rem;border:1px solid var(--cw-line);border-radius:8px;background:#fff;
      cursor:pointer;font:inherit;font-weight:600;font-size:.9rem;color:#2a2a2a;line-height:1.2;
      text-align:center;transition:border-color .15s,box-shadow .15s,background .15s}
    .cw-po__btn small{font-weight:600;font-size:.72em;color:var(--cw-accent)}
    .cw-po__btn.is-active{border-color:var(--cw-accent);box-shadow:inset 0 0 0 1px var(--cw-accent);background:#fff}
    .cw-po__btn.is-active small{color:var(--cw-accent)}
    .cw-po__body{margin-top:.6rem}
    .cw-po__price{font-size:1.15rem;font-weight:700;color:var(--cw-accent);line-height:1.1}
    .cw-po__note{font-size:.8rem;color:#666;margin-top:.15rem}
    .cw-po__freq{margin-top:.55rem}
    .cw-po__freq label{display:block;font-size:.75rem;color:#666;margin-bottom:.25rem}
    .cw-po__freq select{width:100%;padding:.5rem .6rem;border:1px solid var(--cw-line);border-radius:7px;font:inherit;background:#fff}
    .cw-po__freq[hidden],.cw-po__ot[hidden]{display:none}
    .cw-po__ot{margin-top:.6rem}
  </style>

  <div class="cw-po" data-cw-po data-section="{{ cw_uid }}">
    <div class="cw-po__seg">
      <button type="button" class="cw-po__btn is-active" data-mode="subscription">
        Subscribe and Save <small data-cw-save></small>
      </button>
      <button type="button" class="cw-po__btn" data-mode="onetime">
        One time
      </button>
    </div>
    <div class="cw-po__body">
      <div class="cw-po__price" data-cw-price></div>
      <div class="cw-po__note" data-cw-note></div>

      <div class="cw-po__freq" data-cw-freq>
        <label for="cw-freq-{{ cw_uid }}">Delivery frequency</label>
        <select id="cw-freq-{{ cw_uid }}" data-cw-plan>
          {%- for plan in cw_group.selling_plans -%}
            <option value="{{ plan.id }}">{{ plan.name }}</option>
          {%- endfor -%}
        </select>
      </div>

      {%- comment -%} Shows only when "One time" is selected. Paste your installment HTML between the two marks. {%- endcomment -%}
      <div class="cw-po__ot" data-cw-ot hidden>
        <!-- INSTALLMENT HTML START -->

        <!-- INSTALLMENT HTML END -->
      </div>
    </div>
  </div>

  <script>
    (function(){
      var DATA = {
        {%- for variant in cw_product.variants -%}
        "{{ variant.id }}": {
          oneTime: {{ variant.price | json }},
          oneTimeStr: {{ variant.price | money | json }},
          plans: {
            {%- for alloc in variant.selling_plan_allocations -%}
            "{{ alloc.selling_plan.id }}": { price: {{ alloc.price | json }}, priceStr: {{ alloc.price | money | json }}, name: {{ alloc.selling_plan.name | json }} }{% unless forloop.last %},{% endunless %}
            {%- endfor -%}
          }
        }{% unless forloop.last %},{% endunless %}
        {%- endfor -%}
      };
      var SECTION = {{ cw_uid | json }};

      function init(){
        var root = document.querySelector('[data-cw-po][data-section="'+SECTION+'"]');
        if(!root || root.dataset.cwInit) return;
        var form = (root.closest('form[data-type="add-to-cart-form"]'))
          || (root.closest('.shopify-section, product-information, .product, [class*="product"]') || document).querySelector('form[data-type="add-to-cart-form"]')
          || document.querySelector('form[data-type="add-to-cart-form"]');
        if(!form) return;
        root.dataset.cwInit = "1";

        var variantInput = form.querySelector('input[name="id"]');
        var planSelect = root.querySelector('[data-cw-plan]');
        var priceEl = root.querySelector('[data-cw-price]');
        var noteEl = root.querySelector('[data-cw-note]');
        var freqEl = root.querySelector('[data-cw-freq]');
        var otEl = root.querySelector('[data-cw-ot]');
        var saveEl = root.querySelector('[data-cw-save]');
        var btns = root.querySelectorAll('.cw-po__btn');
        var mode = 'subscription';

        function vData(){
          var keys = Object.keys(DATA);
          var vid = variantInput && DATA[variantInput.value] ? variantInput.value : keys[0];
          return DATA[vid] || {};
        }
        function planInput(create){
          var i = form.querySelector('input[name="selling_plan"][data-cw]');
          if(!i && create){ i=document.createElement('input'); i.type='hidden'; i.name='selling_plan'; i.setAttribute('data-cw',''); form.appendChild(i); }
          return i;
        }
        function saveText(vd, id){
          var p=vd.plans&&vd.plans[id]; if(!p||!vd.oneTime) return '';
          var pct=Math.round((1-(p.price/vd.oneTime))*1000)/10;
          return pct>0?('save '+pct+'%'):'';
        }
        function render(){
          var vd=vData();
          var id=planSelect?planSelect.value:(vd.plans?Object.keys(vd.plans)[0]:'');
          btns.forEach(function(b){var on=b.dataset.mode===mode;b.classList.toggle('is-active',on);});
          if(mode==='subscription'){
            freqEl.hidden=false; if(otEl) otEl.hidden=true;
            var p=vd.plans&&vd.plans[id];
            priceEl.textContent=p?p.priceStr:(vd.oneTimeStr||'');
            var freqText=planSelect&&planSelect.selectedIndex>-1?planSelect.options[planSelect.selectedIndex].textContent:'';
            if(freqText.indexOf(',')>-1) freqText=freqText.slice(freqText.lastIndexOf(',')+1).trim();
            noteEl.textContent=freqText;
            if(saveEl) saveEl.textContent=saveText(vd,id);
            var inp=planInput(true); if(inp) inp.value=id;
          } else {
            freqEl.hidden=true; if(otEl) otEl.hidden=false;
            priceEl.textContent=vd.oneTimeStr||'';
            noteEl.textContent='One-time purchase';
            if(saveEl) saveEl.textContent='';
            var ex=planInput(false); if(ex) ex.remove();
          }
        }
        btns.forEach(function(b){b.addEventListener('click',function(){mode=b.dataset.mode;render();});});
        if(planSelect) planSelect.addEventListener('change',render);
        form.addEventListener('change',function(){setTimeout(render,60);});
        render();
      }
      if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',init);
      else init();
      document.addEventListener('shopify:section:load',init);
    })();
  </script>
{%- endif -%}

Adjust the size

  • Button text size: the font-size:.9rem on .cw-po__btn
  • Price size: the font-size:1.15rem on .cw-po__price
  • Accent colour: the --cw-accent value on the first CSS line

Tested on a Horizon store: the slim selector keeps Subscribe and One time in one row, and the installment box renders only in One time with the subscription plan removed from the cart.

Regards,
Ajay,
Pinflow - Pinterest Virtual Assistant