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 : 1 rem 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 : 1 rem ;
}
.shopify_subscriptions_purchase_option_wrapper label {
display : flex ;
align-items : center ;
gap : 0.5 rem ;
cursor : pointer ;
font-weight : 500 ;
}
.shopify_subscriptions_app_block_label {
border-bottom : 1 px 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 1 rem 1 rem 2 rem ;
margin : 0 ;
}
.shopify_subscriptions_app_block_label_children li {
padding : 0.5 rem 0 ;
}
.shopify_subscriptions_app_block_label_children label {
display : flex ;
align-items : center ;
gap : 0.5 rem ;
cursor : pointer ;
}
.shopify_subscriptions_in_widget_price {
font-weight : 600 ;
color : var ( --color-sale );
}
.allocation_price {
font-size : 0.9 em ;
}
/* 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."
}
}
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 : 768 px ) {
.shopify_subscriptions_purchase_option_wrapper {
flex-direction : column ;
align-items : flex-start ;
gap : 0.5 rem ;
}
.shopify_subscriptions_in_widget_price {
width : 100 % ;
text-align : left ;
}
}
Best Practices
Performance : Lazy-load subscription options JavaScript to improve page speed
Accessibility : Use semantic HTML and proper ARIA labels for radio buttons
Price Updates : Ensure prices update when variants or selling plans change
Clear Messaging : Display subscription terms and cancellation policies
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