Skip to main content

Theme Extension

The theme extension provides Liquid-based UI components that integrate subscription purchase options into Shopify storefronts. It displays selling plan groups created by merchants and allows customers to select subscription frequencies during product browsing.

Extension Configuration

extensions/theme-extension/shopify.extension.toml
name = "theme-extension"
type = "theme"
Theme extensions use the theme type and consist of Liquid templates, JavaScript, and CSS assets.

Structure

The theme extension follows Shopify’s theme app extension structure:
theme-extension/
├── shopify.extension.toml
├── blocks/
│   └── app-block.liquid        # Main subscription UI block
├── assets/
│   ├── app-block.js            # Client-side functionality
│   └── styles.css              # Custom styles
└── locales/
    └── en.default.json         # Translations

App Block

The main Liquid template renders subscription options:
extensions/theme-extension/blocks/app-block.liquid
{% liquid
  assign product = block.settings.product
  assign current_variant = product.selected_or_first_available_variant
%}

{% if product.selling_plan_groups.size > 0 %}
  <div class="shopify_subscriptions_app_container" 
       data-section-id='{{ section.id }}' 
       data-product-id='{{ product.id }}'>
    <script src="{{ 'app-block.js' | asset_url }}" defer></script>
    <link href="{{ 'styles.css' | asset_url }}" rel='stylesheet' type="text/css"/>
    
    {% for variant in product.variants %}
      {% if variant.selling_plan_allocations.size > 0 %}
        <section data-variant-id='{{ variant.id }}' 
                 class='shopify_subscriptions_app_block {% if variant.id != current_variant.id %}shopify_subscriptions_app_block--hidden{% endif %}'>
          <fieldset class="shopify_subscriptions_fieldset">
            <div style='border-color:{{ block.settings.dividers_color }}; 
                        background: {{ block.settings.bacgkround_color }}; 
                        border-radius: {{ block.settings.border_radius }}px;'>
              
              {% unless product.requires_selling_plan %}
                <div class='shopify_subscriptions_purchase_option_wrapper'>
                  <label>
                    <input
                      type='radio'
                      name="purchaseOption_{{ section.id }}_{{ variant.id }}"
                      class='shopify_subscriptions_app_block_one_time_purchase_option'
                      id='{{ block.id }}_one_time_purchase'
                      data-radio-type='one_time_purchase'
                      data-variant-id='{{ variant.id }}'
                      checked
                    />
                    {{ 'product.purchase_options.one_time_purchase' | t }}
                  </label>
                  <div class='shopify_subscriptions_in_widget_price'>
                    {{ variant.price | money_with_currency }}
                  </div>
                </div>
              {% endunless %}
              
              {% assign group_ids = variant.selling_plan_allocations | map: 'selling_plan_group_id' | uniq %}
              {% for group_id in group_ids %}
                {% assign group = product.selling_plan_groups | where: 'id', group_id | first %}
                {% assign allocations = variant.selling_plan_allocations | where: 'selling_plan_group_id', group_id %}
                
                <div class='shopify_subscriptions_app_block_label'>
                  <div class='shopify_subscriptions_purchase_option_wrapper'>
                    <label>{{ group.name }}</label>
                    <div class='shopify_subscriptions_in_widget_price allocation_price' 
                         id='{{ block.id }}_{{ group_id }}_{{ variant.id }}_allocation_price'>
                      {{ allocations.first.price | money_with_currency }}
                    </div>
                  </div>
                  
                  <ul class='shopify_subscriptions_app_block_label_children'>
                    {% for allocation in allocations %}
                      <li>
                        <label>
                          <input
                            type='radio'
                            name="purchaseOption_{{ section.id }}_{{ variant.id }}"
                            data-radio-type='selling_plan'
                            data-selling-plan-id='{{ allocation.selling_plan.id }}'
                            data-variant-price='{{ allocation.price | money_with_currency | escape }}'
                          />
                          {{ allocation.selling_plan.name }}
                        </label>
                      </li>
                    {% endfor %}
                  </ul>
                </div>
              {% endfor %}
            </div>
          </fieldset>
        </section>
      {% endif %}
    {% endfor %}
  </div>
{% endif %}

Key Features

One-Time Purchase

Option to purchase without subscription (unless product requires subscription)

Multiple Plans

Display multiple selling plan groups per product

Variant Support

Show variant-specific subscription options and pricing

Dynamic Pricing

Display discounted prices for subscription options

JavaScript Functionality

The app-block.js file handles client-side interactions:
extensions/theme-extension/assets/app-block.js
(function() {
  // Handle variant changes
  function onVariantChange(variantId) {
    // Hide all subscription blocks
    document.querySelectorAll('.shopify_subscriptions_app_block').forEach(block => {
      block.classList.add('shopify_subscriptions_app_block--hidden');
    });
    
    // Show block for selected variant
    const variantBlock = document.querySelector(
      `[data-variant-id="${variantId}"]`
    );
    if (variantBlock) {
      variantBlock.classList.remove('shopify_subscriptions_app_block--hidden');
    }
  }
  
  // Handle selling plan selection
  function onSellingPlanChange(event) {
    const input = event.target;
    const sellingPlanId = input.dataset.sellingPlanId;
    const variantPrice = input.dataset.variantPrice;
    
    // Update form hidden input
    const form = input.closest('form');
    let sellingPlanInput = form.querySelector('input[name="selling_plan"]');
    
    if (!sellingPlanInput) {
      sellingPlanInput = document.createElement('input');
      sellingPlanInput.type = 'hidden';
      sellingPlanInput.name = 'selling_plan';
      form.appendChild(sellingPlanInput);
    }
    
    if (input.dataset.radioType === 'selling_plan') {
      sellingPlanInput.value = sellingPlanId;
      updatePriceDisplay(variantPrice);
    } else {
      sellingPlanInput.value = '';
    }
  }
  
  // Update displayed price
  function updatePriceDisplay(price) {
    const priceElement = document.querySelector('.product-price');
    if (priceElement) {
      priceElement.textContent = price;
    }
  }
  
  // Initialize
  document.addEventListener('DOMContentLoaded', function() {
    // Listen for variant changes
    const variantSelector = document.querySelector('[name="id"]');
    if (variantSelector) {
      variantSelector.addEventListener('change', (e) => {
        onVariantChange(e.target.value);
      });
    }
    
    // Listen for selling plan selection
    document.querySelectorAll('[data-radio-type]').forEach(input => {
      input.addEventListener('change', onSellingPlanChange);
    });
  });
})();

Styling

Customize the appearance with CSS:
extensions/theme-extension/assets/styles.css
.shopify_subscriptions_app_container {
  margin: 1rem 0;
}

.shopify_subscriptions_fieldset {
  border: none;
  padding: 0;
  margin: 0;
}

.shopify_subscriptions_app_block {
  display: block;
}

.shopify_subscriptions_app_block--hidden {
  display: none;
}

.shopify_subscriptions_purchase_option_wrapper {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem;
}

.shopify_subscriptions_purchase_option_wrapper label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
  font-weight: 500;
}

.shopify_subscriptions_app_block_label {
  border-bottom: 1px solid var(--divider-color);
}

.shopify_subscriptions_app_block_label:last-child {
  border-bottom: none;
}

.shopify_subscriptions_app_block_label_children {
  list-style: none;
  padding: 0 1rem 1rem 2rem;
  margin: 0;
}

.shopify_subscriptions_app_block_label_children li {
  padding: 0.5rem 0;
}

.shopify_subscriptions_app_block_label_children label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  cursor: pointer;
}

.shopify_subscriptions_in_widget_price {
  font-weight: 600;
  color: var(--color-sale);
}

.allocation_price {
  font-size: 0.9em;
}

/* Radio button styling */
input[type="radio"] {
  accent-color: var(--color-primary);
  cursor: pointer;
}

Theme Block Settings

Merchants can customize the appearance through theme editor settings:
{
  "name": "Subscription Options",
  "settings": [
    {
      "type": "product",
      "id": "product",
      "label": "Product"
    },
    {
      "type": "color",
      "id": "background_color",
      "label": "Background color",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "dividers_color",
      "label": "Divider color",
      "default": "#e5e5e5"
    },
    {
      "type": "color",
      "id": "color_text_body",
      "label": "Text color",
      "default": "#000000"
    },
    {
      "type": "range",
      "id": "border_radius",
      "label": "Border radius",
      "min": 0,
      "max": 20,
      "step": 1,
      "default": 8,
      "unit": "px"
    },
    {
      "type": "range",
      "id": "border_thickness",
      "label": "Border thickness",
      "min": 0,
      "max": 5,
      "step": 1,
      "default": 1,
      "unit": "px"
    }
  ]
}

Localization

Add translations for the subscription UI:
extensions/theme-extension/locales/en.default.json
{
  "product": {
    "purchase_options": {
      "one_time_purchase": "One-time purchase",
      "subscription": "Subscription"
    }
  },
  "policy": {
    "description": "Subscriptions can be paused, skipped, or cancelled at any time from your account."
  }
}

Integration with Product Forms

Ensure the subscription selection integrates with the add-to-cart form:
<form action="/cart/add" method="post" class="product-form">
  <input type="hidden" name="id" value="{{ product.selected_or_first_available_variant.id }}">
  
  <!-- Variant selector -->
  <select name="id">
    {% for variant in product.variants %}
      <option value="{{ variant.id }}">{{ variant.title }}</option>
    {% endfor %}
  </select>
  
  <!-- Render subscription block -->
  {% render 'subscription-options', product: product %}
  
  <!-- Hidden input for selling plan (populated by JavaScript) -->
  <input type="hidden" name="selling_plan" value="">
  
  <button type="submit">Add to cart</button>
</form>

Subscription Badge

Add a subscription badge to product cards in collection pages:
{% if product.selling_plan_groups.size > 0 %}
  <span class="subscription-badge">
    {{ 'product.purchase_options.subscription' | t }}
  </span>
{% endif %}

Customization Examples

Custom Discount Display

Show subscription savings prominently:
{% for allocation in allocations %}
  {% if allocation.compare_at_price > allocation.price %}
    {% assign savings = allocation.compare_at_price | minus: allocation.price %}
    {% assign savings_percent = savings | times: 100 | divided_by: allocation.compare_at_price %}
    <span class="subscription-savings">Save {{ savings_percent }}%</span>
  {% endif %}
{% endfor %}

Highlighted Subscription Option

<div class="subscription-highlight">
  <span class="badge">Most Popular</span>
  <label>
    <input type="radio" name="purchase_option" />
    {{ allocation.selling_plan.name }}
  </label>
</div>

Delivery Schedule Preview

<div class="delivery-preview">
  <p>You'll receive deliveries:</p>
  <ul>
    {% assign billing_policy = allocation.selling_plan.billing_policy %}
    {% for i in (1..3) %}
      {% assign days = i | times: billing_policy.interval_count %}
      {% if billing_policy.interval == 'week' %}
        {% assign days = days | times: 7 %}
      {% elsif billing_policy.interval == 'month' %}
        {% assign days = days | times: 30 %}
      {% endif %}
      {% assign delivery_date = 'now' | date: '%s' | plus: days | times: 86400 | date: '%B %d, %Y' %}
      <li>{{ delivery_date }}</li>
    {% endfor %}
  </ul>
</div>

Conditional Display

Only show subscriptions for eligible products:
{% if product.tags contains 'subscription-eligible' and product.selling_plan_groups.size > 0 %}
  <!-- Render subscription options -->
{% endif %}

Mobile Optimization

Ensure responsive design:
@media (max-width: 768px) {
  .shopify_subscriptions_purchase_option_wrapper {
    flex-direction: column;
    align-items: flex-start;
    gap: 0.5rem;
  }
  
  .shopify_subscriptions_in_widget_price {
    width: 100%;
    text-align: left;
  }
}

Best Practices

  1. Performance: Lazy-load subscription options JavaScript to improve page speed
  2. Accessibility: Use semantic HTML and proper ARIA labels for radio buttons
  3. Price Updates: Ensure prices update when variants or selling plans change
  4. Clear Messaging: Display subscription terms and cancellation policies
  5. Visual Hierarchy: Make subscription benefits prominent to encourage adoption

Testing

Test the theme extension across scenarios:
  • Products with and without selling plan groups
  • Multi-variant products
  • Products that require subscriptions
  • Mobile and desktop viewports
  • Different themes (Dawn, Debut, etc.)
  • Cart additions with selected selling plans

Admin Actions

Create and configure selling plan groups displayed in the theme

Buyer Subscriptions

Customer management interface for active subscriptions

Build docs developers (and LLMs) love