Subscriptions and selling plans
Signal renders full subscription support on the product page: a purchase-type toggle, a frequency choice, prices that match what checkout will actually charge, and a savings badge derived from real numbers rather than read out of a plan name.
The control is rendered by the Buy buttons block and self-hides completely on products with no selling plans — so a catalogue with one subscription product doesn’t carry the UI on the other four hundred.
What you need first
Section titled “What you need first”Shopify models subscriptions as selling plan groups containing selling plans, and only an app can create them. The theme renders what the app has set up; it cannot create a plan.
-
Install a subscription app. Shopify’s own Shopify Subscriptions app is free and the simplest starting point. Third-party apps (Recharge, Loop, Seal) work identically as far as the theme is concerned.
-
Create a selling plan group. In Shopify Subscriptions this is a subscription plan: give it a name (“Resupply”), add one or more delivery frequencies, and set the discount on each — a percentage, a fixed amount off, or a flat replacement price.
-
Assign products to it. Either from the plan in the app, or from the product’s own page in admin. A product can belong to more than one group.
-
Check the product page. The purchase-type toggle appears under the variant picker, inside the buy form. No theme setting needs switching on.
Settings
Section titled “Settings”Two labels, both on the Buy buttons block under a Subscriptions header:
| Setting | Type | Default | What it does |
|---|---|---|---|
| Delivery label | Text | Delivery | The heading above the control. “Delivery”, “Resupply”, “How often” |
| One-time label | Text | One time | The label on the one-time purchase option |
Everything else — the frequency names, the discounts, the groups — comes from the app. There is nothing in the theme to keep in sync, which is the point.
What the shopper sees
Section titled “What the shopper sees”The purchase type is one decision
Section titled “The purchase type is one decision”Two options side by side: One time (selected by default) and Subscribe. When a subscription discount exists, the Subscribe option carries a badge reading Save up to 15% — the best saving available across every plan on the product.
The frequency choice stays hidden until Subscribe is picked. That’s the whole reason the toggle exists. An earlier build flattened groups and plans into one row of pills, so a product in two groups with seven plans became eight equal-width boxes each labelled with Shopify’s auto-generated sentence (“Deliver every 2 months, 10% off”). The group names vanished and the row wrapped into a slab of text. Purchase type is one decision; frequency is a different one; they’re now presented as two.
Frequency: rows up to four, a select beyond
Section titled “Frequency: rows up to four, a select beyond”Each frequency is a row with its name, its price under that plan, and a savings badge when there is one:
Every month $204.00 15% offEvery 2 months $216.00 10% offEvery 3 months $228.00 5% offWhen the product belongs to more than one group and the groups are named, each group gets a small heading above its rows — so “Resupply” and “Seasonal” stay distinguishable.
The rows become a single <select> with an <optgroup> per selling plan group:
<select> <optgroup label="Resupply"> <option>Every month — $204.00 (15% off)</option> <option>Every 2 months — $216.00 (10% off)</option> … </optgroup> <optgroup label="Seasonal"> … </optgroup></select>Past four, pills stop being scannable and become a wall. A select is also the better control for one-of-many on a phone, where it opens the native picker instead of a grid of tap targets — and the <optgroup> is what keeps the group names visible rather than collapsing them into a flat list.
The threshold is the total number of plans across all groups, so a product in three groups with two plans each (six total) gets the select.
Plan names
Section titled “Plan names”Each row or option is labelled with the plan’s first option value when it has one — “Every month” — and falls back to the plan’s full name otherwise. Shopify’s generated plan names read like sentences (“Deliver every 1 month, 15% off”); using the option value keeps the label short and lets the price and the badge carry the rest.
Prices come from the allocation, not the variant
Section titled “Prices come from the allocation, not the variant”This is the part worth understanding, because it’s where subscription themes usually go wrong.
A plan’s price adjustment can be a percentage, a fixed amount off, or an outright replacement price. You cannot reliably reconstruct the final price from the plan definition. What you can read is variant.selling_plan_allocations — Shopify’s own calculation of what each plan costs for each variant.
Signal emits that as a map inside the control:
{ "40123456789": { "501": 20400, "502": 21600 }, "40123456790": { "501": 20400, "502": 21600 }}Variant id → plan id → price in cents. Every price the page shows under a subscription is read from this map:
- the price block figure
- the struck-through figure beside it
- the add-to-bag button label
- the sticky bar price
- each frequency row’s own price and savings badge
Before this, the page always rendered the one-time price, which meant a shopper on a 10% plan was quoted a figure the checkout did not charge. Now the number on screen is the number billed.
Variant-specific plans are handled
Section titled “Variant-specific plans are handled”Plans can apply to some variants and not others, and the frequency labels are rendered for the first variant. So when the shopper changes variant, Signal re-labels every frequency row and every <option> from the same map. Where a variant has no allocation for a plan, the one-time price is used as the fallback rather than a stale figure from another variant.
The savings badge
Section titled “The savings badge”Derived, never parsed:
percent off = round((variant price − allocation price) × 100 / variant price)Comparing the two prices is the one reading that’s true for all three adjustment types. A badge reading 0% is hidden rather than rendered, and the Save up to badge on the Subscribe toggle is the maximum of these across every plan.
The strike-through follows what’s displayed
Section titled “The strike-through follows what’s displayed”A subscription discount is itself worth striking through. The compare-at element compares against whatever price is currently shown, not against the variant price — so on a subscription the one-time price appears struck through beside the discounted one, and on a sale item under a plan the higher of compare-at and one-time is used.
Subscription-only products
Section titled “Subscription-only products”When a product has requires_selling_plan set — Shopify’s “this product can only be sold as a subscription” flag — the control changes shape:
- The One time option is not rendered at all. Offering it would let a shopper choose something that fails at add-to-cart.
- The frequency choice is visible immediately, not hidden behind a toggle.
- The first plan of the first group is pre-selected, so the form is valid from the first paint.
How the choice reaches the cart
Section titled “How the choice reaches the cart”The visible controls are a UI over one hidden input:
<input type="hidden" name="selling_plan" value="" data-sub-input>The chosen plan is always submitted with the order, whether the shopper adds to bag normally or through a native form post. “One time” is expressed as an empty value rather than a sentinel plan id, which is what lets the frequency rows and the select share one target.
Signal’s quick-add path (the [data-add] buttons on cards and in the drawer) reads the same input and only attaches selling_plan to the request when the value is non-empty.
Where the plan shows up afterwards
Section titled “Where the plan shows up afterwards”The plan name is printed under the variant title on every subscription line, in all four places a line item appears:
| Where | What it shows |
|---|---|
| Cart drawer | Plan name under the variant, server-rendered and in the client-rendered fallback |
| Cart page | Plan name under the variant |
| Customer order page | Plan name under the line item |
| Checkout | Shopify’s own subscription summary |
All of them read item.selling_plan_allocation.selling_plan.name — so a shopper reviewing their bag can see which subscription they picked, not just that they picked one.